тип_результата ID_функции(список параметров)




{

код функции

return выражение;

}

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

Тип результата определяет тип выражения, значение которого возвращается в точку ее вызова при помощи оператора return выражение; (возврат). Выражение преобразуется к типу_результата, указанному в заголовке функции и передается в точку вызова. Тип возвращаемого функцией значения может быть любым базовым типом, а также указателем на массив или функцию. Если функция не должна возвращать значение, указывается тип void. В данном случае оператор return можно не ставить. Из функции, которая не описана как void, необходимо возвращать значение, используя оператор return. Если тип функции не указан, то по умолчанию устанавливается тип int.

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

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

Функция может не иметь параметров, но круглые скобки необходимы в любом случае. Если у функции отсутствует список параметров, то при декларации такой функции желательно в круглых скобках указать void. Например, void main (void){... }.

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

Пример функции, определяющей наименьшее значение из двух целочисленных переменных:

int min (int x, int y)

{

return (x<y)? x: y;

}

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

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

 

11.2. Вызов функции

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

Простейший вызов функции имеет следующий формат:

ID_функции (список аргументов);

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

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

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

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

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

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

#include <stdio.h>

void f1(int);

void main(void)

{

f1(5);

}

void f1(int i)

{

int m=0;

puts(" n m p ");

while (i--) {

static int n = 0;

int p = 0;

printf(" %d %d %d \n", n++, m++, p++);

}

}

Статическая переменная n будет создана в сегменте данных ОП и проинициализируется нулем только один раз при первом выполнении оператора, содержащего ее определение, т.е. при первом вызове функции f 1. Автоматическая переменная m инициализируется при каждом входе в функцию. Автоматическая переменная р инициализируется при каждом входе в блок цикла.

В результате выполнения программы получим

n m p

0 0 0

1 1 0

2 2 0

3 3 0

4 4 0

 

11.3. Передача аргументов в функцию

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

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

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

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

Пример функции, в которой меняются местами значения x и y:

void zam(int *x, int *y)

{

int t = *x;

*x = *y;

*y = t;

}

Участок программы с обращением к данной функции:

void zam (int*, int*);

void main (void)

{

int a=2, b=3;

printf(" a = %d, b = %d\n", a, b);

zam (&a, &b);

printf(" a = %d, b = %d\n", a, b);

}

При таком способе передачи данных в функцию их значения будут изменены, т.е. на экран монитора будет выведено

a = 2, b=3

a = 3, b=2

Если требуется запретить изменение значений какого-либо параметра внутри функции, то в его декларации используют атрибут const, например:

void f1(int, const double);

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

 

11.4. Операция typedef

Любому типу данных, как стандартному, так и определенному пользователем, можно задать новое имя с помощью операции typedef:

typedef тип новое_имя;

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

typedef unsigned int UINT; – здесь UINT – новое имя;

typedef char M_s [101]; – здесь M_s – тип пользователя, определяющий строки, длиной не более 100 символов.

Декларации объектов введенных типов будут иметь вид

UINT i, j; ® две переменные типа unsigned int;

M_s str[10]; ® массив из 10 элементов, в каждом из которых можно хранить по 100 символов.

Рассмотренная операция упростит использование указателей на функции, которые рассматриваются в следующем разделе.

 

11.5. Указатели на функции

В языке Си идентификатор функции является константным указателем на начало функции в оперативной памяти и не может быть значением переменной. Но имеется возможность декларировать указатели на функции, с которыми можно обращаться как с переменными (например, можно создать массив, элементами которого будут указатели на функции).

Рассмотрим методику работы с указателями на функции.

1. Как и любой объект языка Си, указатель на функции необходимо декларировать. Формат объявления указателя на функции следующий:

тип (* переменная-указатель)(список параметров);

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

Например, объявление вида

double (* p_f)(char, double);

говорит о том, что декларируется указатель p_f, который можно устанавливать на функции, возвращающие результат типа double и имеющие два параметра: первый – символьного типа, а второй – вещественного типа.

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

переменная-указатель = ID_функции;

Например, имеется функция с прототипом: double f1(char, double); тогда операция

p_f = f1;

установит указатель p_f на данную функцию.

3. Вызов функции после установки на нее указателя выглядит так:

(*переменная-указатель)(список аргументов);

или

переменная-указатель (список аргументов);

После таких действий кроме стандартного обращения к функции:

ID_функции(список аргументов);

появляется еще два способа вызова функции:

(*переменная-указатель)(список аргументов);

или

переменная-указатель (список аргументов);

Последняя запись справедлива, так как p_f также является адресом начала функции в оперативной памяти.

Для нашего примера к функции f 1 можно обратиться следующими способами:

f1(‘z’, 1.5); – обращение к функции по имени (ID);

(* p_f)(‘z’, 1.5); – обращение к функции по указателю;

p_f(‘z’, 1.5); – обращение к функции по ID указателя.

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

Пример: написать функцию вычисления суммы sum, обозначив слагаемое формальной функцией fun (x). При вызове функции суммирования передавать через параметр реальное имя функции, в которой задан явный вид слагаемого. Например, пусть требуется вычислить две суммы:

и .

Поместим слагаемые этих сумм в пользовательские функции f 1 и f 2 соответственно. При этом воспользуемся операцией typedef, введя пользовательский тип данных: указатель на функции p_f, который можно устанавливать на функции, возвращающие результат double и имеющие один параметр типа double.

Тогда в списке параметров функции суммирования достаточно будет указать фактические идентификаторы функций созданного типа p_f.

Текст программы для решения данной задачи может быть следующим:

...

typedef double (*p_f)(double);

double sum(p_f, int, double); // Декларации прототипов функций

double f1(double);

double f2(double);

void main(void)

{

double x, s1, s2;

int n;

puts (" Введите кол-во слагаемых n и значение x: ");

scanf (" %d %lf ", &n, &x);

s1 = sum (f1, 2*n, x);

s2 = sum (f2, n, x);

printf("\n\t N = %d, X = %lf ", n, x);

printf("\n\t Сумма 1 = %lf\n\t Сумма 2 = %lf ", s1, s2);

}

/* Первый параметр функции суммирования – формальное имя функции, введенное с помощью typedef типа */

double sum(p_f fun, int n, double x) {

double s=0;

for(int i=1; i<=n; i++)

s+=fun(x);

return s;

}

//–––––––––––––– Первое слагаемое –––––––––––––––––––

double f1(double r) {

return r/5.;

}

//–––––––––––––– Второе слагаемое ––––––––––––––––––––

double f2(double r) {

return r/2.;

}

 

В заключение рассмотрим оптимальную передачу в функции одномерных и двухмерных массивов.

Передача в функцию одномерного массива:

void main(void)

{

int vect[20];

fun(vect);

}

void fun(int v[ ]) {

}

При использовании в качестве параметра одномерного массива в функцию передается указатель на его первый элемент, т.е. массив всегда передается по адресу и параметр v[ ] преобразуется в *v. Поэтому этой особенностью можно воспользоваться сразу:

void fun(int *v) {

}

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

void fun(int v[20]) {

...

}

В случае передачи массива символов, т.е. строки, ее фактическую длину можно определить по положению признака окончания строки (нуль-символа) через стандартную функцию strlen.

 

Передача в функцию двухмерного массива:

Если размеры известны на этапе компиляции, то

void f1(int m[3][4]) {

int i, j;

for (i = 0; i<3; i++)

for (j = 0; j<4; j++)

... // Обработка массива

}

Двухмерный массив, как и одномерный, также передается как указатель, а указанные размеры используются просто для удобства записи. При этом первый размер массива не используется при поиске положения элемента массива в ОП, поэтому передать массив можно так:

void main(void)

{

int mas [3][3]={{1,2,3}, {4,5,6}};

fun (mas);

}

void fun(int m[ ][3]) {

}

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

void fun(int**, int, int);

void main()

{

int **mas, n, m;

...

fun (mas, n, m);

}

void fun(int **m, int n, int m) {

... // Обработка массива

}

 

11.6. Рекурсивные функции

Рекурсивной (самовызываемой или самовызывающей) называют функцию, которая прямо или косвен­но вызывает сама себя.

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

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

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

В рекурсивных функциях необходимо выполнять следующие правила.

1. При каждом вызове в функцию передавать модифицированные данные.

2. На каком-то шаге должен быть прекращен дальнейший вызов этой функции, это значит, что рекурсивный процесс должен шаг за шагом упрощать задачу так, чтобы для нее появилось нерекурсивное решение, иначе функция будет вызывать себя бесконечно.

3. После завершения очередного обращения к рекурсивной функции в вызывающую функцию должен возвращаться некоторый результат для дальнейшего его использования.

Пример 1. Заданы два числа a и b, большее из них разделить на меньшее, используя рекурсию.

Текст программы может быть следующим:

...

double proc(double, double);

void main (void)

{

double a,b;

puts(“ Введи значения a, b: ”);

scanf(“%lf %lf”, &a, &b);

printf(“\n Результат деления: %lf”, proc(a,b));

}

//––––––––––––––––––– Функция –––––––––––––––––––––––

double proc(double a, double b) {

if (a< b) return proc (b, a);

else return a/b;

}

Если a больше b, условие, поставленное в функции, не выполняется и функция proc возвращает нерекурсивный результат.

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

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

double fact (int k) {

if (k < 1) return 1;

else

return k * fact (k – 1);

}

Для нулевого значения параметра функция возвращает 1 (0! = 1), в противном случае вызывается та же функция с уменьшенным на 1 значени­ем параметра и результат умножается на текущее значение па­раметра. Тем самым для значения параметра k организуется вычисление произведения

k * (k –1) * (k –2) *... * 3 * 2 * 1 * 1

Последнее значение «1» – результат выполнения условия k < 1 при k = 0, т.е. последовательность рекурсивных обращений к функции fact прекращается при вызове fact (0). Именно этот вызов приводит к последнему значению «1» в произведении, так как последнее выражение, из которого вызывается функция, имеет вид: 1 * fact (1 – 1).

Пример 3. Рассмотрим функ­цию определения корня уравнения f (x) = 0 на отрезке [ а, b ] с заданной точностью eps. Предположим, что ис­ходные данные задаются без ошибок, т.е. eps > 0, f (a)* f (b) < 0, b > а, и вопрос о возможности существования нескольких кор­ней на отрезке [ а, b ] нас не интересует. Не очень эффективная рекурсивная функция для решения поставленной задачи приведена в следующей программе:

...

int counter = 0; // Счетчик обращений к тестовой функции

//–––––––– Нахождение корня методом деления отрезка пополам ––––––––––

double Root(double f(double), double a, double b, double eps) {

double fa = f(a), fb = f(b), c, fc;

if (fa * fb > 0) {

printf("\n На интервале a,b НЕТ корня!");

exit(1);

}

с = (а + b) / 2.0;

fc = f(c);

if (fc == 0.0 || fabs(b – a) < = eps) return c;

return (fa * fс < 0.0)? Root(f, a, c, eps): Root(f, c, b, eps);

}

//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––

void main()

{

double x, a=0.1, b=3.5, eps=5е–5;

double fun(double); // Прототип тестовой функции

x = Root (fun, a, b, eps);

printf ("\n Число обращений к функции = %d. ", counter);

printf ("\n Корень = %lf. ", x);

}

//–––––––––––––– Определение тестовой функции fun –––––––––––––––––

double fun (double x) {

counter++; // Счетчик обращений – глобальная переменная

return (2.0/x * соs(х/2.0));

}

Значения a, b и eps заданы постоянными только для тестового анализа полученных результатов, хотя лучше данные вводить с клавиатуры.

В результате выполнения программы с определенными в ней конкретными данными получим:

Число обращений к функции = 54.

Корень = 3.141601.

Неэффективность предложенной программы связана, например, с излишним количеством обращений к программной реализации функции, для которой определяется корень. При каждом рекурсивном вызове функции Root повторно вычисляются значения f (a) и f (b), хотя они уже известны после предыдущего вызова.

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

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

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

 

11.7. Параметры командной строки функции main

В стандарте ANSI функция main возвращает целочисленный результат, т.е. используется следующим образом:

int main () {

¼

return 0;

}

здесь оператор return возвращает операционной системе код завершения функции, причем значение 0 трактуется как нормальное завершение, остальные значения воспринимаются как ошибки.

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

int main (int argc, char *argv[])...

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

Функция main может иметь и третий параметр argp, который служит для передачи параметров операционной системы (ОС), в которой выполняется программа, в этом случае ее заголовок имеет вид

int main (int argc, char *argv[], char *argp[])

Операционная система поддерживает передачу значений для параметров argc, argv, argp, а пользователь отвечает за передачу и использование фактических аргументов функции main.

Приведем пример программы печати фактических аргументов, передаваемых из ОС в функцию main и параметров оперативной системы.

int main (int argc, char *argv[], char *argp[])

{

int i;

printf ("\n Program Name %s", argv[0]);

for(i=1; i <=argc; i++)

printf ("\n Argument %d = %s", i, argv[i]);

printf ("\n OC parametrs: ");

while (*argp) {

printf ("\n %s", *argp);

argp++;

}

return 0;

}

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

 

ГЛАВА 12. Классы памяти и область действия объектов

 

12.1. Классы памяти объектов в языке Cи

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

Класс памяти программного объекта определяет время ее существования (время жизни) и область видимости (действия) и может принимать одно из значений: auto, extern, static и register.

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

Область действия объекта – это часть кода программы, в которой его можно использовать для доступа к связанному с ним участку памяти. В зависимости от области действия переменная может быть локальной (внутренней) или глобальной (внешней).

Имеется три основных участка программы, где можно декларировать переменные:

– внутри функции (блока);

– в заголовке функции при определении параметров;

– вне функции.

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

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

Областью действия глобальных данных считается файл, в котором они определены, от точки описания до его окончания.

Если класс памяти у переменной не указан явным образом, он определяется компилятором исходя из контекста ее декларации.

Время жизни может быть постоянным – в течение выполнения программы, и временным – в течение выполнения функции (блока) программы.

 

12.2. Автоматические переменные

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

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

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

По умолчанию локальные объекты, объявленные в коде функции, имеют атрибут класса памяти auto.

Принадлежность к этому классу можно также подчеркнуть явно, например:

void main(void) {

auto int max, lin;

...

}

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

Для глобальных переменных этот атрибут не используется.

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

Регистровая память позволяет увеличить быстродействие программы, но к размещаемым в ней объектам в языке Си (но не С++) не применима операция получения адреса «&».

 

12.3. Статические и внешние переменные

Объекты, размещаемые в статической памяти, декларируются с атрибутом static и могут иметь любой атрибут области действия. В зависимости от расположения оператора описания статические переменные могут быть глобальными и локальными. Время жизни – постоянное. Инициализируется один раз при первом выполнении оператора, содержащего определение переменной. Глобальные объекты всегда являются статическими. Атрибут static, использованный при описании глобального объекта, предписывает ограничение его области применения только в пределах остатка текущего файла, а значения локальных статических объектов сохраняются до повторного вызова функции, т.е. в языке Си ключевое слово static имеет разный смысл для локальных и глобальных объектов.

Итак, переменная, описанная вне функции, является внешней (глобальной) переменной.

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

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

При отсутствии явной инициализации для внешних и статических переменных гарантируется их обнуление. Автоматические и регистровые переменные имеют неопределенные начальные значения («мусор »).

Внешняя переменная должна быть определена вне всех функций. При этом ей выделяется фактическое место в памяти. Такая переменная должна быть описана в той функции, которая собирается ее использовать. Это можно сделать либо явным описанием extern, либо по контексту.

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

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

Важно различать описание внешней переменной и ее определение. Описание указывает свойство переменной, ее размер, тип и т. д.; определение же вызывает еще и отведение ей участка оперативной памяти. Например, если вне какой-либо функции появляются инструкции

int sp;

double val[20];

то они определяют внешние переменные sp и val, вызывают отведение памяти для них и служат в качестве описания для остальной части этого исходного файла. В то же время строчки:

extern int sp;

extern double val [ ];

описывают в остальной части этого исходного файла переменную sp как int, а vаl как массив типа double, но не создают переменных и не отводят им места в памяти.

Во всех файлах, составляющих исходную программу, должно содержаться только одно определение внешней переменной. Другие файлы могут содержать описание extern для доступа к ней.

Любая инициализация внешней переменной проводится только в декларации. В декларации должны указываться размеры массивов, а в описании extern этого можно не делать.

Например, в основном файле проекта:

int sp = 50;

double val [20];

void main() {

...

а в дополнительном файле этого проекта:

extern int sp;

extern double val [ ];

...

В Си есть возможность с помощью директивы компилятору # include использовать во всей программе только одну копию описаний extern и присоединять ее к каждому файлу во время его препроцессорной обработки.

Если переменная с таким же идентификатором, как внешняя, декларирована в функции без указания extern, то тем самым она становится внутренней (локальной) для данной функции.

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

Пример, иллюстрирующий использование внешних данных:

Основной файл проекта Дополнительный файл
¼ int x, y; char str[ ] = “Rezult = ”; void fun1(void); void fun2(void); void fun3(void); void main(void) { fun1(); fun2(); fun3(); } void fun1(void) { y = 15; printf(“\n %s %d\n”, str, y); } ¼ extern int x, y; extern char str[ ]; int r = 4;   void fun2(void) { x = y / 5 + r; printf(“ %s %d\n”, str, x); }   void fun3(void) { int z= x + y; printf(“ %s %d\n”, str, z); }  

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

Rezult = 15

Rezult = 7

Rezult = 22

 

12.4. Область действия переменных

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

Напомним общую структуру исходного текста программы:

<директивы препроцессора>

<описание глобальных объектов>

<заголовок функции>

{

<описание локальных объектов>

<список инструкций>

}

Файл исходного текста может включать любое количество определений функций и/или глобальных данных.

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

#include<stdio.h>

int f1(int);

int f2(int);

int f3(int);

int n; // Глобальная n

void main (void)

{

int i=2; // Локальная i =2

n=3; // Глобальная n =3

i = f1(i); // Обе переменные i – локальные

printf(" 1: i=%d, n=%d\n",i,n); // i =7, n =3

n = f1(i); // n – глобальная, i – локальная

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

i = f2(n); // i и n – локальные

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

i = f3(i); // Обе переменные i – локальные

printf(" 4: i=%d, n=%d\n",i,n); // i =29, n =14

}

//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––

int f1(int i) { // Параметр функции i – локальная

int n = 5; // n – локальная

n+=i;

return n;

}

//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––

int f2(int n) { // Параметр функции n – локальная

n+=3;

return n;

}

//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––

int f3(int i) {

n+=2; // n – глобальная

return i+n;

}

 

Существуют следующие области действия: блок, файл, функция, прототип функции, область структурированных типов данных.

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

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

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

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

Структурированный тип данных. Элементы структур и объединений являются видимыми лишь в их пределах. Они образуются при создании переменной указанного типа и разрушаются при ее уничтожении.

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

void main(void)

{

int a = 3;

printf(“\n Block 1: %d “, a); {

double a = 2.5;

printf(“\n Block 2: %lf “, a); {

char a = ‘A’;

printf(“\n Block 3: %c “, a);

}

printf(“\n New Block 2: %lf“, a+1.25);

}

printf(“\n New Block 1: %d“, ++a);

}

Результат программы:

Block 1: 3

Block 2: 2.5

Block 3: A

New Block 2: 3.75

New Block 1: 4

 

 

Советы по программированию

При выполнении вариантов заданий придерживайтесь следующих клю­че­вых моментов.

1. Размеры нединамических массивов задаются константами или константными выражениями. Рекомендуется для этого использовать поименованные константы.

2. Элементы массивов нумеруются с нуля, максимальный номер (индекс) элемента всегда на единицу меньше указанного размера.

3. Автоматический контроль выхода индексов элементов за указанные границы массива отсутствует.

4. Указатель – это переменная, в которой хранится адрес участка оперативной памяти.

5. Имя массива является указателем на его нулевой элемент, т.е. на его начало в оперативной памяти.

6. Обнуления динамической памяти при ее выделении не происходит. Инициализировать динамический массив нельзя.

7. Освобождение памяти, выделенной посредством стандартных функций, выполняется при помощи функции free (при использовании операции new – операцией delete).

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



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2016-02-16 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: