Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Text_lektsy_Si-dlya_studentov.pdf
Скачиваний:
6
Добавлен:
08.05.2015
Размер:
1.22 Mб
Скачать

void main(void)

{ … }

Функции в Си совершенно равноправны. И функция main тоже может вызываться

другими функциями:

 

Например:

 

int func (int a);

 

int main (void)

{

return func (5);

}

int func (int a)

{

return main ( );

}

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

7.2.1Передача скаляров

ФАКТИЧЕСКИЕ ПАРАМЕТРЫ. Перед передачей входные аргументы типа float и char АВТОМАТИЧЕСКИ преобразуются в типы double и int, следовательно, соответствующие параметры должны быть описаны надлежащим образом, иначе произойдет

ошибка.

 

 

 

Пример:

 

 

 

Вызывающая процедура

Вызываемая процедура

 

 

float a, b, max();

float max (float a, float b) { //Ошибка

..................

 

 

 

y=2+3.5*max(a, b);

float max (double a, double b) {//Верно

ФОРМАЛЬНЫЕ ПАРАМЕТРЫ. При передаче СКАЛЯРНЫХ ДАННЫХ в вызываемую процедуру передаются только копии аргументов. Физически происходит выделение нового участка оперативной памяти и копиро-

Аргумент1

 

 

Буфер1

вание аргументов в память, распределенную вызы-

 

 

 

 

 

ваемой процедуре.

 

 

 

 

ВНИМАНИЕ! Поэтому в теле функции мы мо-

АргументN

 

 

БуферN

 

 

жем изменить ТОЛЬКО КОПИЮ и не повлияем на

 

 

 

 

 

АРГУМЕНТ в вызывающей процедуре!

Вопрос? А как же передать результаты работы в вызывающую процедуру, если реально можно передать только одно значение?

Для этого надо передавать АДРЕС области памяти, где хранится аргумент. При этом функция не может изменить этот адрес, а содержание может. Для получения адреса операция &. Следовательно, соответствующий параметр – УКАЗАТЕЛЬ!!

Пример:

Дана матрица {a[i][j]}, i,j = 1...10.

Найти max{ a [i][j] } и его индексы.

Вызывающая процедура void main (void) {

float maxmatr(), maxim, a[10][10]; int m, n;

42

..................................

maxim = maxmatr (a, &m, &n);

Функция

float maxmatr (float a[10][10], int *k, int *l ){ float max;

int i, j;

max = a[0][0];

for( *k = *l = i =0; i<10; i++ ){ for ( j = 0; j < 10; j++ ) {

if ( max<a[i][j] ) { max=a[i][j];

*k=i;

*l=j;} } }

return max;

}/* End maxmatr */

Подпрограммы

void maxmatr (float a[10][10], int *k, int *l, float *max) { /* float max; везде заменить max на *max и убрать return/

Примером использования ПП может служить scanf: список данных – выходные аргументы, поэтому надо адреса(&), printf: список данных – входные аргументы, поэтому значения, без адресов.

7.2.2.Передача массивов

Фактическим аргументом при передаче массива служит его имя, т.е. передается АДРЕС 1-го элемента (индексы–0) и массив НЕ КОПИРУЕТСЯ в локальную память функции.

Это передача ПО ИМЕНИ | ПО ССЫЛКЕ | ПО АДРЕСУ. Такой механизм присущ передаче всех данных в языке FORTRAN.

При описании вида float a[10][10];

обращение к функции вида: <имя_функции> ( a ) эквивалентно обращению: <имя_функции> ( &a[0][0] ).

Следовательно, массивы-параметры занимают память, отводимую в вызывающей процедуре массивам-аргументам, поэтому в вызываемой процедуре допустимы описания вида:

int myfun ( float b[], a[][10] );

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

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

43

Пример: Аргументы

Параметры

float a[5][5], b[36];

float a[], b[][6];

Вычислить z = uT b u , где{u[i]}, i=1...4; {b[i][j]}, i, j = 1...4.

/* Вычисление квадратичной формы */ void main(void) {

float

u[4], /* Входной вектор */

b[4][4], /* Входная матрица */

v[4],

/* Вектор b*u */

z, /* Результат */

scalar( );/* Скалярное произведение векторов */ int i, j;

printf ( "Исходный вектор: \n" ); for ( i=0; i<4; i++ ) {

scanf ( "%f", &u[i] ); } printf ( "Исходная матрица: \n" ); for ( i=0; i<4; i++ ) {

for ( j=0; j<4; j++ ){ scanf ( "%f", &b[i][j] );

} }

matrix(b, u, v); z = scalar(v, u);

printf ( "\n \n \n Квадратичная форма равна %.5g \n", z );

}/* End main */

/* Умножение матрицы на вектор */

void matrix ( float a[][4], float x[], float y[] ) { int i, j;

for ( i=0; i<4; i++ ) {

for ( y[i]=j=0; j<4; j++ ) { y[i] += a[i][j]*x[j];

} }

}/* End matrix */

/* Скалярное произведение векторов */ float scalar ( float x[], float y[] ){ int i;

for ( z = i = 0; i<4; i++ ) { z += x[i]*y[i];

}

return z;

}/* End scalar */

44

Глава 8 ПРЕПРОЦЕССОР

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

Препроцессорные директивы (#include, #define и т.д.) управляют преобразованием текста программы до ее компиляции.

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

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

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

Исходная программ, подготовленная на языке СИ в виде текстового файла, походит три последовательных этапа обработки:

Препроцессорное преобразование текста.

Компиляция.

Компоновка (редактирование связей или сборка).

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

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

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

Все системно–зависимые обозначения (например, системно–зависимый индикатор конца строки) перекодируются в стандартные коды.

Каждая пара из символов ‘\’ и “конец строки” вместе с пробелами между ними убираются, и тем самым следующая строка исходного текста присоединяется к строке, в которой находилась эта пара символов.

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

Выполняются директивы препроцессора и производятся макроподстановки (в зависимости от конкретной программы). Препроцессор “сканирует” исходный текст программы в поиске строк, начинающихся с символа #. Такие строки воспринимаются препроцессором как команды (директивы), которые определяют действия по преобразованию текста:

Замена идентификаторов (обозначений) заранее подготовленными последовательностями символов.

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

Исключение из программы отдельных частей ее текста (условная

компиляция).

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

Esc-последовательности в символьных константах и символьных строках,

45

например: ‘\n’ или ‘\xF2’, заменяются на их эквиваленты (на соответствующие числовые коды).

Смежные символьные строки (строковые константы) конкатенируются, т.е. соединяются в одну строку.

Каждая препроцессорная лексема преобразуется в лексему языка СИ.

Формат директивы препроцессора:

# имя_директивы лексемы_препроцессора

Перед символом # и после него в директиве разрешены пробелы. Пробелы также разрешены перед лексемами_препроцессора, между ними и после их.

Окончанием препроцессорной директивы служит конец текстовой строки (при наличии символа ‘\’, обозначающего перенос строки, окончанием препроцессорной директивы будет признак конца следующей строки текста).

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

Определены следующие препроцессорные директивы:

#define

–определение макроса или препроцессорного идентификатора.

#include

–включение текста из файла.

#undef

–отмена определения макроса или идентификатора (препроцессорного)

#if

–проверка условия–выражения.

#ifdef

–поверка определенности идентификатора.

#ifndef

–проверка неопределенности идентификатора.

#else

–начало альтернативной ветви для #if.

#endif

–окончание условной директивы #if.

#elif

–составная директива #else/#if

#line

–смена номера следующей ниже строки.

#error

–формирование текста сообщения об ошибке трансляции.

#pragma

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

#–пустая директива.

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

Директива #include позволяет включать в текст программы текст из указанного

файла.

Директива #undef отменяет действие директивы #define, которая определила до этого имя препроцессорного идентификатора.

Директива #if и ее модификации #ifdef, #ifndef свместно с директивами #else,

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

46

Директива #line позволяет управлять нумерацией строк в файле с программой. Имя файла и желаемый начальный номер строки указывается непосредственно в дирек-

тиве #line.

Директива #error позволяет задать текст диагностического сообщения, которое выводится при возникновении ошибок.

Директива #pragma вызывает действия, зависящие от реализации, т.е. запланированные авторами программы.

Директива # ничего не вызывает, т.к. является пустой директивой, т.е. не дает никакого эффекта и всегда игнорируется.

8.1Макроопределения

Начнем с самой простой – с макроопределений констант. Мы можем задать какуюлибо константу как

const int a = 5;.

В таком случае a – это полноценная переменная, с собственным адресом и областью видимости. Более того, написав

*((int*) &a) = 6;,

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

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

Для замены выбранного программистом идентификатора заранее подготовленной последовательностью символов используется директива (обратите внимание на пробелы):

#define <идентификатор> <строка_замещения>

(обратите внимание НА ОТСУТСТВИЕ ТОЧКИ С ЗАПЯТОЙ). Символы пробелов, помещенные в начале и в конце строки замещения, в подстановке не используются.

Например: #define VAR 5

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

Где действует макроопределение? Ввиду того, что замена производится до компиляции, областей видимости на этот этап времени еще нет. Поэтому макроопределение действительно с момента его объявления на протяжении всего текста программы до конца файла или явной отмены определения директивой #undef:

Например:

#undef VAR

Исходный текст

Результат препроцессорной обработки

#define BEGIN {

 

#define END {

 

void main()

void main()

47

begin

{

операторы

операторы

end

}

В данном случае программист решил использовать в качестве операторных скобок идентификаторы begin и end. До компиляции препроцессор заменяет все вхождения этих идентификаторов стандартными скобками { и }. Соответствующие указания программист дал препроцессору с помощью директив #define.

Данные ранее определения переопределять нельзя. Если это надо все же сделать, требуется сначала отменить определение:

Например: #define VAR 2

...

#undef VAR 2 #define VAR 3

....

#undef VAR 3 #define VAR 4

И, наконец, определение не должно превышать одну строку. Если <строка_замещения> оказывается слишком длинной, то ее можно продолжить в следующей строке текста. Для этого в конце продолжаемой строки помещается символ ‘\’. В ходе одной из стадий препроцессорной обработки этот символ вместе с последующим символом конца строки будет удален из текста программы.

Пример:

#define STRING “\n Game Over! \

– \t Игра закончена!”

printf(STRING);

На экране будет выведено: Game Over! – Игра закончена!

К склейке применимы два правила:

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

2)Склейка удаляет только символ новой строки и никак не затрагивает символы, идущие после нее (например, табуляции, помещенные с целью повышения читаемости

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

Пример:

#define LONG_STR "Это длинная строка..." "...очень длинная строка..."

"ну просто очень длинная строка."

Для всего остального лишняя табуляция препятствием не является.

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

явно определить на этапе препроцессорной обработки:

Исходный текст Результат препроцессорной обработки

48

#define K 40

 

void main()

void main()

{

{

int M[K][K];

int M[40][40];

float A[2*K+1];

float A[2*40+1];

float B[K+3][K–3];

float B[40+3][40–3];

#define M 16 /* Идентификатор M определен как int 16*/

#define M 'c' /* Идентификатор определен как симв. const*/

#define

M

"c" /* Идентификатор определен как симв. строка

с двумя элементами 'c', '\0' */

#define

PK

printf ( \n Номер элемента = %d", i )

 

 

int i = 4;

На экране:

PK

 

Номер элемента = 4

#define REAL long double

#define E (5+10) // Подстановка в программе 2*E приведет

#define E 5+10 // к разным результатам

#define PI 3.141592

Макрозамены не производятся в строковых константах: #include <stdio.h>

#define str строка void main (void) {

printf ("Это моя str.\n"); // Печатается "Это моя str."

}

#include <stdio.h> #define str "строка" void main (void) {

printf ("Это моя %s.\n", str); // Печатается "Это моя строка."

}

Макрозамена будет произведена только во втором случае.

8.2 Включение в программу заголовочных файлов

Директива #include <…> предназначена для включения в текст программы текста файла из каталога “заголовочных файлов”, поставляемых вместе со стандартными библиотеками компилятора.

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

Список заголовочных файлов для стандартных библиотек определен стандартом языка.

#include <имя_заголовочного_файла>

49

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

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

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

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

Термин “заголовочный файл” (header file) в применении к файлам содержащим описания библиотечных функций стандартных библиотек, не случаен.

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

Хотя заголовочный файл может быть включен в программу не в ее начале, а непосредственно перед обращением к нужной библиотечной функции, такое размещение директив #include <…> не рекомендуется.

Команда #include имеет ТРИ ФОРМАТА записи:

#include <имя_файла> /*имя в угловых скобках*/

#include “имя_файла” /*имя в кавычках*/

#include имя_макроса /*макрос, расширяемый до обозначения файла*/

где <имя_макроса> – это введенный директивой #define предпроцессорный идентификатор либо макрос, при замене которого после конечного числа подстановок будет получена последовательность символов <имя_файла> либо “имя_файла”.

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

50

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]