Главная страница

Курс на Си. Подбельский. Курс программирования на Си. В., Фомин С. С. Курс программирования на языке Си Учебник


Скачать 1.57 Mb.
НазваниеВ., Фомин С. С. Курс программирования на языке Си Учебник
АнкорКурс на Си
Дата18.02.2023
Размер1.57 Mb.
Формат файлаdocx
Имя файлаПодбельский. Курс программирования на Си.docx
ТипУчебник
#943863
страница18 из 42
1   ...   14   15   16   17   18   19   20   21   ...   42

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

Для обеспечения мобильности программ с функциями, имеющи­ми изменяемые списки аргументов, в каждый компилятор стандарт языка предлагает включать специальный набор макроопределений, которые становятся доступными при включении в текст програм­мы заголовочного файла stdarg.h. Макрокоманды, обеспечивающие простой и стандартный (не зависящий от реализации) способ до­ступа к конкретным спискам аргументов переменной длины, имеют следующий формат:

void va_start(va_list param, последний_явный_параметр);

type va_arg(va_list param, type);

void va_end(va_list param);

Кроме перечисленных макросов, в файле stdarg.h определен специальный тип данных va_list, соответствующий потребностям обработки переменных списков параметров. Именно такого типа должны быть первые аргументы, используемые при обращении к макрокомандам va_start( ), va_arg( ), va_end( ). Кроме того, для обращения к макросу va_arg( ) необходимо в качестве второго аргу­мента использовать обозначение типа (type) очередного аргумента, к которому выполняется доступ. Объясним порядок использования перечисленных макроопределений в теле функции с переменным количеством аргументов (рис. 5.2). Напомним, что каждая такая функция с переменным количеством аргументов должна иметь хотя бы один явно специфицированный параметр, за которым после за­пятой стоит многоточие. В теле функции обязательно определяется объект типа va_list. Например, так:

va_list factor;

Определенный таким образом объект factor обладает свойствами указателя. С помощью макроса va_start( ) объект factor связывает­ся с первым необязательным параметром, то есть с началом списка неизвестной длины. Для этого в качестве второго аргумента при

va_arg (factor, type)

Реальные аргументы обязательные необязательные

int

int

float

int

char

int[]

long double

N

К

sum

Z

cc

ro

smell

va_list factor; /* Определили указатель */

va_start (factor, sum); /* Настройка указателя (см. ниже) */

| N | К | sum | Z | се | го | smell |

int ix;

ix=va_arg (factor, int); /* Считали в ix значение Z типа int, одновременно сместили указатель на длину переменной типа int */

N | К | sum | Z | ее | го | smell |

char сх;

cx=va_arg (factor, char); /* Считали в сх значение сс типа char и сместили указатель на длину объекта типа char */

| N | К | sum | Z | сс | го | smell |

int * рг;

pr=va_arg(factor, int); /* Считали в рг адрес го типа int* массива типа int [ ] и сместили указатель factor на размер указателя int* */

long double рр;

pp=va_arg (factor, long double); /* Считали значение smell */

Рис. 5.2. Схема обработки переменного списка параметров
с помощью макросов стандартной библиотеки


обращении к макросу va_start( ) используется последний из явно специфицированных параметров функции (предшествующий мно­готочию):

va_start (factor, последний_явный_параметр);

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

Теперь с помощью разыменования указателя factor мы можем по­лучить значение первого аргумента из переменного списка. Однако нам неизвестен тип этого аргумента. Как и без использования макро­сов, тип аргументов нужно каким-то образом передать в функцию. Если это сделано, то есть определен тип type очередного аргумента, то обращение к макросу позволяет, во-первых, получить значение очередного (сначала первого) аргумента типа type. Вторая задача макрокоманды va_arg( ) - заменить значение указателя factor на адрес следующего аргумента в списке. Теперь, узнав каким-то обра­зом тип, например type1, этого следующего аргумента, можно вновь обратиться к макросу:

va_arg (factor, type1)

Это обращение позволяет получить значение следующего аргу­мента и переадресовать указатель factor на аргумент, стоящий за ним в списке, и т. д.

Макрокоманда va_end( ) предназначена для организации кор­ректного возврата из функции с переменным списком аргументов. Ее единственным аргументом должен быть указатель типа va_list, который использовался в функции для перебора параметров. Та­ким образом, для наших иллюстраций вызов макрокоманды должен иметь вид:

va_end (factor);

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

Макрокоманда va_end( ) обычно модифицирует свой аргумент (указатель типа va_list), и поэтому его нельзя будет повторно ис­пользовать без предварительного вызова макроса va_start( ).

Примеры функций с переменным количеством аргументов. Для иллюстрации особенностей использования описанных макросов рас­смотрим функцию, формирующую в динамической памяти массив из элементов типа double. Количество элементов определяется зна­чением первого обязательного параметра функции, имеющего тип int. Значения элементов массива передаются в функцию с помощью переменного числа необязательных аргументов типа double. Текст функции вместе с основной программой, из которой выполняется ее вызов:

#include /* Для макросредств */

#include /* Для функции calloc( ) */ double * set_array (int k, ...)

{

int i;

va_list par; /* Вспомогательный указатель */ double * rez; /* Указатель на результат */ rez=calloc(k,sizeof(double));

if (rez == NULL) return NULL;

va_start (par, k); /* "Настройка" указателя */ /* Цикл "чтения" параметров: */

for (i=0; i
rez[i]=va_arg (par, double);

va_end (par);

return rez;

}

#include

void main( )

{

double * array; int j;

int n=5;

printf("\n");

array=set_array (n, 1.0, 2.0, 3.0, 4.0, 5.0);

if (array == NULL) return;

for (j=0; j
printf ("\t%f", array [j]);

free(array);

}

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

1.000000 2.000000 3.000000 4.000000 5.000000

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

В функциях с переменным количеством аргументов есть один уязвимый пункт - если при «чтении» аргументов с помощью мак­роса va_arg( ) указатель выйдет за пределы явно использованного списка аргументов, то результат, возвращаемый макросом va_arg( ), не определен. Таким образом, в нашей функции set_array( ) коли­чество явно заданных аргументов переменного списка ни в коем случае не должно быть меньше значения первого аргумента (заме­няющего int k).

В основной программе main( ) определен указатель double * array, которому присваивается результат (адрес динамического массива), возвращаемый функцией set_array( ). Затем с помощью указателя array в цикле выполняется доступ к элементам массива, и их значе­ния выводятся на экран дисплея.

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

#include

#include /* Для функций обработки строк */

#include /* Для макросредств */

#include /* Для функции malloc( ) */

char *concat(char *s1, ...)

{

va_list par;/* Указатель на аргументы списка */

char *cp = s1;

char *string;

int len = strlen(s1);/* Длина 1-го аргумента */ va_start(par, s1); /* Начало переменного списка */

/* Цикл вычисления общей длины строк: */ while (cp = va_arg(par, char *))

len += strlen(cp);

/* Выделение памяти для результата: */

string = (char *)malloc(len + 1);

strcpy(string, s1); /* Копируем 1-й параметр */ va_start(par, s1); /* Начало переменного списка */

/* Цикл конкатенации строк: */ while (cp = va_arg(par, char *))

strcat(string, cp); /* Конкатенация двух строк */ va_end(par);

return string;

}

void main()

{

char* concat(char* s1, ...); /* Прототип функции */

char* s; /* Указатель для результата */

s=concat("\nNulla ","Dies ","Sine ", "Linea!", NULL);

s = concat(s,

" - Ни одного дня без черточки!",

"\n\t",

"(Плиний Старший о художнике Апеллесе)", NULL); printf("\n%s",s);

}

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

Nulla Dies Sine Linea! - Ни одного дня без черточки! (Плиний Старший о художнике Апеллесе)

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

printf(char* format, ...);

scanf(char* format, ...);

В обеих функциях форматная строка, связанная с указателем format, содержит спецификации преобразования (%d - для деся­тичных чисел, - для вещественных данных в форме с плавающей точкой, %f - для вещественных значений в форме с фиксирован­ной точкой и т. д.). Кроме того, эта форматная строка в функции printf( ) может содержать произвольные символы, которые выво­дятся на дисплей без какого-либо преобразования. Чтобы продемон­стрировать особенности построения функций с переменным числом аргументов, классики языка Си [1] рекомендуют самостоятельно написать функцию, подобную функции printf( ). Последуем их со­вету, но для простоты разрешим использовать только спецификации преобразования «%d» и «%f».

/* Упрощенный аналог printf( ).

По мотивам K&R, [2], стр. 152 */

#include

#include /* Для макросредств

переменного списка параметров */

void miniprint(char *format, ...) {

va_list ap;/* Указатель на необязательный параметр*/

char *p; /* Для просмотра строки format */

int ii; /* Целые параметры */

double dd; /* Параметры типа double */

va_start(ap,format);/Настроились на первый параметр */ for (p = format; *p; p++)

{

if (*p != '%') {

printf("%c",*p);

continue;

}

switch (*++p)

{ case 'd': ii = va_arg(ap,int);

printf("%d",ii);

break;

case 'f': dd = va_arg(ap,double);

printf("%f",dd);

break;

default: printf("%c",*p);

} /* Конец переключателя */

}/* Конец цикла просмотра строки-формата */ va_end(ap);/*Подготовка к завершению функции */ } void main() {

void miniprint(char *, ...); /* Прототип */ int k = 154;

double e = 2.718282;

miniprint("\nЦелое k= %Н\Гчисло e= %f",k,e);

}

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

Целое k= 154, число e= 2.718282

Интересной особенностью предложенной функции miniprint( ) и ее серьезных прародителей - библиотечных функций языка Си printf( ) и scanf( ) - является использование одного явного пара­метра и для задания типов последующих аргументов, и для опре­деления их количества. Для этого в строке, определяющей формат вывода, записывается последовательность спецификаций, каждая из которых начинается символом '%'. Предполагается, что количество спецификаций равно количеству аргументов в следующем за фор­матом списке. Конец обмена и перебора аргументов определяется по достижении конца форматной строки, когда *p = = '\0'.

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

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

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

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

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

long fact(int k)

{ if (k < 0) return 0;

if (k == 0) return 1;

return k * fact(k-1);

}

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

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

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

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

double expo(double a, int n) { if (n == 0) return 1;

if (a == 0.0) return 0;

if (n > 0) return a * expo(a, n-1);

if (n < 0) return expo(a, n+1) / a;

}

При обращении вида expo(2.0, 3) рекурсивно выполняются вызовы функции expo( ) с изменяющимся вторым аргументом: expo(2.0,3), expo(2.0,2), expo(2.0,1), expo(2.0,0). При этих вызовах последовательно вычисляется произведение

2.0 * 2.0 * 2.0 * 1

и формируется нужный результат.

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

expo(5.0,-2)

эквивалентен вычислению выражения

expo(5.0,0) / 5.0 / 5.0

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

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

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

#include

#include /*Для математических функций*/

#include /* Для функции exit() */

/* Рекурсивная функция для определения корня математической функции методом деления пополам: */

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

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

if (fa * fb > 0) {

printf("\пНеверен интервал локализации "

"корня!");

exit(1);

}

c = (a + b)/2.0;

fc = f(c);

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

return (fa * fc < 0.0) ? recRoot(f, a, c,eps): recRoot(f, c, b,eps); }

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

{

double root, A=0.1, /* Левая граница интервала */ B = 3.5, /* Правая граница интервала */

EPS = 5e-5; /* Точность локализации корня */

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

root = recRoot(giper, A, B, EPS);

рг^ТСХпЧисло обращений к тестовой функции " "= %d",counter);

printf("\nKopeHb = %f",root);

}

/* Определение тестовой функции: */ double giper(double x)

{

counter++; /*Счетчик обращений - глобальная переменная */ return (2.0/x * cos(x/2.0));

}

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

Число обращений к тестовой функции = 54 Корень = 3.141601

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

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

В литературе по программированию рекурсиям уделено достаточ­но внимания как в теоретическом плане, так и в плане рассмотре­ния механизмов реализации рекурсивных алгоритмов. Сравнивая рекурсию с итерационными методами, отмечают, что рекурсивные алгоритмы наиболее пригодны в случаях, когда поставленная зада­ча или используемые данные определены рекурсивно (см., напри­мер, Вирт Н. Алгоритмы + структуры данных = программы. - М.: Мир, 1985. - 406 с.). В тех случаях, когда вычисляемые значения определяются с помощью простых рекуррентных соотношений, го­раздо эффективнее применять итеративные методы. Таким образом, определение корня математической функции, возведение в степень и вычисление факториала только иллюстрируют схемы организации рекурсивных функций, но не являются примерами эффективного применения рекурсивного подхода к вычислениям.

    1. Классы памяти

и организация программ

Локализация объектов. До сих пор в примерах программ мы ис­пользовали в основном только два класса памяти - автоматическую и динамическую память. (В последнем примере из §5.6 использо­вана глобальная переменная int counter, которая является внешней по отношению к функциям программ main( ), giper( ).) Для обо­значения автоматической памяти могут применяться специфика­торы классов памяти auto или register. Однако и без этих ключе­вых слов-спецификаторов класса памяти любой объект (например, массив или переменная), определенный внутри блока (например, внутри тела функции), воспринимается как объект автоматической памяти. Объекты автоматической памяти существуют только внут­ри того блока, где они определены. При выходе из блока память, выделенная объектам типа auto или register, освобождается, то есть объекты исчезают. При повторном входе в блок для тех же объек­тов выделяются новые участки памяти, содержимое которых никак не зависит от «предыстории». Говорят, что объекты автоматической памяти локализованы внутри того блока, где они определены, а вре­мя их существования (время «жизни») определяется присутствием управления внутри этого блока. Другими словами, автоматическая память всегда внутренняя, то есть к ней можно обратиться только в том блоке, где она определена.

Пример:

#include

/* Переменные автоматической памяти */ void autofunc(void)

{

int K=1;

printf("\tK=%d",K);

K++;

return;

}

void main()

{

int i;

for (i=0;i<5;i++) autofunc();

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

K=1 К=1 K=1 К=1 K=1

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

#include

/* Локальные переменные статической памяти */ void stat(void)

{

static int K=1;

printf("\tK=%d",K);

K++;

return;

}

void main()

{

int i;

for (i=0;i<5;i++) stat();

}

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

K=1 K=2 K=3 K=4 K=5

Отличие функций autofunc( ) и stat( ) состоит в наличии специ­фикатора static при определении переменной int K, локализованной в теле функции stat( ). Переменная К в функции autofunc( ) - это переменная автоматической памяти, она определяется и инициали­зируется при каждом входе в функцию. Переменная static int K по­лучает память и инициализируется только один раз. При выходе из функции stat( ) последнее значение внутренней статической пере­менной К сохраняется до последующего вызова этой же функции. Сказанное иллюстрирует результат выполнения программы.

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

Выше в рекурсивной программе для вычисления приближенного значения корня математической функции переменная counter для подсчета количества обращений к тестовой функции giper( ) опре­делена как глобальная для всех функций программы. Вот еще один пример программы с глобальной переменной:

#include

int N=5; /* Глобальная переменная */ void func(void)

{

printf("\tN=%d",N);

N--;

return;

}

void main()

{

int i;

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

{

func();

N+=2;

}

}

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

N=5 N=6 N=7 N=8 N=9

Переменная int N определена вне функций main( ) и func( ) и является глобальным объектом по отношению к каждой из них. При каждом вызове func( ) значение N уменьшается на 1, в основной программе - увеличивается на 2, что и определяет результат.

Глобальный объект может быть «затенен» или «скрыт» внутри блока определением, в котором для других целей использовано имя глобального объекта. Модифицируем предыдущую программу:

#include

int N=5; /* Глобальная переменная */ void func(void)

{

printf("\tN=%d",N);

N--;

return;

}

void main()

{

int N; /* Локальная переменная */

for (N=0;N<5;N++) func();

}

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

N=5 N=4 N=3 N=2 N=1

Переменная int N автоматической памяти из функции main( ) ни­как не влияет на значение глобальной переменной int N. Это разные объекты, которым выделены разные участки памяти. Внешняя пере­менная N «не видна» из функции main( ), и это результат опреде­ления int N внутри нее.

Динамическая память - это память, выделяемая в процессе вы­полнения программы. А вот на вопрос «Глобальный или локальный объект размещен в динамической памяти?» попытаемся найти пра­вильный ответ.

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

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

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

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

  • указатель определен как локальный объект статической па­мяти. Динамическая память, выделенная однократно в блоке, доступна через указатель при каждом повторном входе в блок. Память нужно освободить только по окончании ее использо­вания;

  • указатель является глобальным объектом по отношению к бло­ку. Динамическая память доступна во всех блоках, где «виден» указатель. Память нужно освободить только по окончании ее использования.

Проиллюстрируем второй вариант, когда объект динамической памяти связан со статическим внутренним (локализованным) ука­зателем:

#include

#include /* Для функций malloc( ), free( ) */ void dynam(void) {

static char *uc=NULL; /* Внутренний указатель */

/* Защита от многократного выделения памяти: */ if(uc == NULL)

{

uc=(char*)malloc(1);

*uc='A';

}

printf("\t%c",*uc);

(*uc)++; return;

}

void main( )

{

int i;

for (i=0; i<5; i++) dynam( );

}

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

A B C D E

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

#include

#include /* Для функций malloc( ), free( ) */

char * uk=NULL; /* Глобальный указатель */

void dynam1 (void)

{

printf ("\t%c", * uk);

(* uk)++;

}

void main (void)

{

int i;

uk=(char*) malloc (1);

*uk='A';

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

{

dynam1( );

(*uk)++;

}

free(uk); }

Результат

выполнения

программы:

A C

E G

I

Динамический объект создается в основной функции и связыва­ется с указателем uk. Там же он явным присваиванием получает начальное значение 'A'. За счет глобальности указателя динамиче­ский объект доступен в обеих функциях main( ) и dynam1( ). При выполнении цикла в функции main( ) и внутри функции dynam1( ) изменяется значение динамического объекта.

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

Внешние объекты. Здесь в конце главы, посвященной функциям, самое подходящее время, чтобы взглянуть в целом на программу, со­стоящую из набора функций, размещенных в нескольких текстовых файлах. Именно такой вид обычно имеет более или менее серьезная программа на языке Си. Отдельный файл с текстами программы иногда называют программным модулем, но нет строгих терминоло­гических соглашений относительно модулей или файлов с текстами программы. На рис. 5.3 приведена схема программы, текст которой находится в двух файлах. Программа, как мы уже неоднократно го­ворили, представляет собой совокупность функций. Все функции внешние, так как внутри функции по правилам языка Си нельзя определить другую функцию. У всех функций, даже размещенных в разных файлах, должны быть различные имена.

Кроме функций, в программе могут использоваться внешние_объ- екты - переменные, указатели, массивы и т. д. Внешние объекты должны быть определены вне текста функций.

Внешние объекты могут быть доступны из многих функций про­граммы, однако эта доступность не всегда реализуется автоматиче­ски - в ряде случаев нужно дополнительное вмешательство про­граммиста. Если объект определен в начале файла с программой, то он является глобальным для всех функций, размещенных в файле, и доступен в них без всяких дополнительных предписаний. (Огра­ничение - если внутри функции имя глобального объекта исполь­зовано в качестве имени внутреннего объекта, то внешний объект становится недостижимым, то есть «невидимым» в теле именно этой функции.) На рис. 5.3:

  • объект X: доступен в f11( ), f12( ) как глобальный; доступен как внешний в файле 2 только в тех функциях, где будет по­мещено описание extern X;

  • объект Y: доступен как глобальный в f21( ) и f22( ); доступен как внешний в тех функциях файла 1, где будет помещено описание extern Y;

  • объект Z: доступен как глобальный в f22( ) и во всех функциях файла 1 и файла 2, где помещено описание extern Z.

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






। Файл 1

Объект X (определение)

— функция fl 1 (...)

прототип f 12

вызов fl 2

фал 2

Объект У(определение)

— функция 121(...) —

прототип fl 1

вызов fl 1

прототип f22

вызов 122(...)

— функция fl2(...)

вызов fl 1
Объект 2(определение)

— функция f22(...) —

прототип fl2

вызов fl 2

Рис. 5.3. Схема программы,
размещенной в двух файлах


Описание внешнего объекта не есть его определение. Помните: в определении объекту всегда выделяется память, и он может быть инициализирован. Примеры определений:

double summa [5];

char D_Phil [ ]="Doctor of Philosophy";

long M=1000;

В описаниях инициализация невозможна, нельзя указать и коли­чество элементов массивов:

extern double summa [ ]; extern char D_Phil [ ]; extern long M;

5.8. Параметры функции main( )

В соответствии с синтаксисом языка Си основная функция каж­дой программы может иметь такой заголовок:

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

Параметр argv - массив указателей на строки; argc - параметр типа int, значение которого определяет размер массива argv, то есть количество его элементов, envp - параметр-массив указателей на символьные строки, каждая из которых содержит описание одной из переменных среды (окружения). Под средой понимается та про­грамма (обычно это операционная система), которая «запустила» на выполнение функцию main( ).

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

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

При запуске программы в командной строке записывается имя выполняемой программы. Вслед за именем можно разместить нуж­ное количество «слов», разделяя их друг от друга пробелами. Каж­дое «слово» из командной строки становится строкой-значением, на которую указывает очередной элемент параметра argv[i], где 0
Как и в каждом массиве, в массиве argv[ ] индексация элементов начинается с нуля, то есть всегда имеется элемент argv[0]. Этот эле­мент является указателем на полное название запускаемой програм­мы. Например, если из командной строки в ОС Windows выпол­няется обращение к программе EXAMPLE из каталога CATALOG, размещенного на диске С, то вызов выглядит так:

C:\CATALOG\EXAMPLE.EXE

Значение argc в этом случае будет равно 1, а строка, которую адре­сует указатель argv[0], будет такой:

"C:\CATALOG\EXAMPLE.EXE"

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

#include

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

{

int i;

for (i=0; i
printf("\n argv [%d] -> %s", i, argv [i];

}

Пусть программа запускается из такой командной строки (в ОС Windows):

C:\VVP\test66 11 22 33

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

argv

[0]

->

C:\VVP\test66.exe

argv

[1]

->

11

argv

[2]

->

22

argv

[3]

->

33

В главной программе main( ) разрешено использовать и третий параметр char * envp[ ]. Его назначение - передать в программу всю информацию об окружении, в котором выполняется програм­ма. Следующий пример иллюстрирует возможности этого третьего параметра функции main( ).

#include

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

int n;

printf("\nnporpaMMa '%s' "

"\пЧисло параметров при запуске - %d", argv[0], argc-1;

for (n=1; n
printf("Хп'М-й параметр: %s", n, argv[n]);

printf("\n\nCnucok переменных окружения:");

for (n=0; envp[n]; n++) printf("\n%s", envp[n]);

}

Пусть программа «запущена» на выполнение из такой командной строки (в ОС Windows):

C:\WWP\TESTPROG\MAINENVP.EXE qqq www

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

Программа 'C:\WWP\TESTPROG\MAINENVP.EXE'

Число параметров при запуске - 2 1-й параметр: qqq 2-й параметр: www

Список переменных окружения:

TMP=C:\WINDOWS\TEMP

PROMPT=$p$g

winbootdir=C:\WINDOWS

COMSPEC=C:\WINDOWS\COMMAND.COM

TEMP=c:\windows\temp

CLASSPATH=.;c:\cafe\java\lib\classes.zip

HOMEDRIVE=c:

HOMEPATH=\cafe\java

JAVA_HOME=c:\cafe\java windir=C:\WINDOWS NWLANGUAGE=English

PATH=C:\CAFE\BIN;C:\CAFE\JAVA\BIN;C:\BC5\BIN;

C:\WINDOWS;C:\WINDOWS\COMMAND;C:\NC;

C:\ARC;C:\DOS;C:\VLM;C:\TC\BIN

CMDLINE=tc

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

Контрольные вопросы

  1. Приведите формат определения функции.

  2. Укажите роль прототипа функции и правила его размещения.

  3. Что такое «спецификация параметров»?

  4. Объясните соотношение между участками памяти, выделяемы­ми для параметров и аргументов функции.

  5. Могут ли определения функции быть вложенными?

  6. Какой способ передачи параметров предусматривает синтаксис языка Си при обращении к функциям?

  7. Припомните последовательность шагов при передаче парамет­ров функции.

  8. Объясните особенность применения указателей в параметрах функции.

  9. Могут ли изменяться аргументы-указатели за счет исполнения операторов тела функции?

  10. Каким образом в теле функции можно получить доступ к внеш­нему, по отношению к функции, объекту, использованному в ка­честве аргумента?

  11. Объясните использование массивов в параметрах функции.

  12. Укажите различия между массивом в месте его определения и массивом-параметром в теле функции.

  13. Допускается ли использование указателей со смещениями вмес­то индексированных переменных в определениях функций?

  14. Соответствует ли действительности следующее положение: «при каждом обращении к функции ее параметрам выделяются участки памяти, полностью независимые и отличные от участков памяти, выделенных для аргументов, использованных в вызове функции»?

  15. В каких случаях необходимо использовать указатели на функции?

  16. Приведите формат прототипа функции с переменным количест­вом аргументов.

  17. Какие требования предъявляются к функциям с переменным списком параметров?

  18. Приведите определение рекурсивной функции.

  19. Какими способами реализуются итерационные и рекурсивные алгоритмы?

  20. Можно ли итерационный алгоритм заменить на адекватный ему рекурсивный?

  21. Объясните различия между прямой и косвенной рекурсией.

  22. Какие спецификаторы классов памяти применяются для обозна­чения автоматической памяти?

  23. Укажите свойства глобального объекта.

  24. Перечислите варианты работы с динамической памятью.

  25. Укажите свойства внешнего объекта.

  26. Перечислите параметры функции main().

  27. Объясните, для чего служат указатель типа va_list и макрос va_arg().

1   ...   14   15   16   17   18   19   20   21   ...   42


написать администратору сайта