Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

книги / Практикум по программированию на языке Си

..pdf
Скачиваний:
23
Добавлен:
12.11.2023
Размер:
3.53 Mб
Скачать

!

возврат (каретки) в начало строки

(carriage return);

!

новая строка

(new line).

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

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

ЗАДАЧА 01-01. Используя функцию puts(), вывести на экран фразу: "From puts(): Hello, World!"

/*01_01.c – простейшая программа */ #include <stdio.h>

int main ()

{

puts("From puts(): Hello, World!"); return 0;

}

Здесь в первой строке комментарий – последовательность символов, помещенная в скобки /* */ (ограниченная разделителями /*

и */).

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

В теле функции main() первый оператор – обращение к библиотечной функции puts(), описание (прототип) которой находится в тексте заголовочного файла stdio.h. Второй оператор return 0; завершает исполнение программы и возвращает нулевое значение среде исполнения.

Приведенный текст программы необходимо с помощью какоголибо текстового редактора ввести в ЭВМ и разместить в рабочем ка-

11

талоге в виде файла, например, с именем 01_01.с. (Именно в файле с таким названием эта программа помещена в каталог электронной поддержки Практикума.)

Итак, файл "01_01.с" является исходным файлом нашей первой программы. Чтобы из исходного файла получить исполнимую программу, необходимо использовать компилятор языка Си. Обратим внимание на некоторые требования Стандарта языка Си, относящиеся к компиляции и выполнению программы.

1.2. Стадии и этапы обработки Си-программ

Напомним, что для языка Си и его реализаций (компиляторов) действует Стандарт ANSI/ISO, которому должны соответствовать все компиляторы языка Си и, естественно, сам язык. Стандарт языка Си не ограничивает (не определяет) конкретных механизмов реализации компиляторов и правил исполнения программ. Но Стандарт предусматривает две стадии обработки каждой Си-программы, которые обеспечиваются соответственно средой трансляции (компиляции + компоновки) и средой исполнения.

Стандарт определяет:

!представление Си-программ;

!синтаксис и ограничения Си-языка;

!семантические правила интерпретации Си-программ;

!представление входных данных, обрабатываемых Си-програм- мами;

!представление выходных данных, формируемых Си-програм- мами;

!ограничения и пределы, налагаемые допустимой реализацией (компилятором) языка Си.

Как уже упомянуто, программа на языке Си может состоять из

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

В тексте программы на Си, подготовленном программистом, обычно присутствуют конструкции языка Си и команды (директивы) препроцессора. Как мы уже упоминали, препроцессорная директива #include <stdio.h> сообщает препроцессору, что в этом месте в текст программы необходимо включить содержимое (текст) заголо-

12

вочного файла stdio.h. Более подробное рассмотрение этапа препроцессорной обработки будет выполнено в темах 3 и 10. Сейчас достаточно понимать, что препроцессор включает вместо директивы #include <stdio.h> некоторый текст с описаниями библиотечных средств ввода-вывода. В нашей программе 01_01.c таким средством служит функция puts(), прототип которой присутствует в тексте заголовочного файла stdio.h.

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

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

Все сказанное относилось к обработке программы в среде трансляции. Дополнительно на этой стадии выдаются диагностические сообщения (на рис. 1.1 не показаны).

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

миромразными путями (рис. 1.2). Во-первых, функция main() может иметь необязательные параметры, которые служат для приема передаваемых в программу аргументов. (Особенности обработки параметров функции main() рассмотрены на примерах в теме 9 (программа 09_17.с)). Второй источник информации для программы – данные, вводимые пользователем в процессе исполнения (получаемые из стандартного входного потока). По умолчанию входной поток

настроенна клавиатуру. Третий источник данных – информация из файлов.

13

Рис. 1.1. Этапы формирования исполнимого модуля программы

14

Рис. 1.2. Обмены с исполняемой программой:

stdin – стандартный входной поток (клавиатура); stdout – стандартный выходной поток (экран дисплея)); stderr – стандартный поток для сообщений об ошибках (экран дисплея)

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

ввыполненном операторе return.

Впроцессе исполнения программы 01_01.с функция puts() выводит фразу "From puts(): Hello, World!" в стандартный выходной поток stdout, который по умолчанию соответствует экрану дисплея.

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

1.3. Компиляция и исполнение программы на Си

Мы предполагаем, что компилятор языка Си доступен читателю. Если вы используете компилятор DJGPP (см. Приложение), то наберите в командной строке сеанса MS-DOS

>gcc –v

15

После нажатия клавиши ENTER на экране появится, например, такое сообщение о версии установленного компилятора:

Reading specs from c:/djgpp/lib/specs gcc version 2.8.1.

Все в порядке. Можно начинать эксперименты.

Прежде всего либо наберите в текстовом редакторе, либо считайте электронную версию программы 01_01.с (см. с. 11) и разместите ее в вашем рабочем каталоге. Пусть имя рабочего каталога MyDir, и он размещен на диске D:. Для компиляции (включающей препроцессорную обработку, собственно компилирование и компоновку, см. рис. 1.1) проще всего использовать команду:

gcc имя_файла.с -o имя_исполнимого_файла.exe

В нашем случае, находясь в своем рабочем каталоге d:\myDir, наберите в командной строке:

>gcc 01_01.c -o test.exe

После нажатия клавиши ENTER начнется работа компилятора. В окне сеанса MS-DOS в верхней строке последовательно появятся следующие сообщения о выполняемых шагах:

gcc

(начало работы компилятора)

gcc -CPP

(препроцессорная обработка)

gcc -CC1

(собственно компиляция)

gcc -AS

(ассемблирование)

gcc -LD

(компоновка)

gcc -STUBIFY

(оформление exe-кода).

Их последовательность соответствует этапам обработки Сипрограммы при формировании исполнимого модуля программы. Если в нашей программе нет ошибок, то в файле test.exe будет создан исполнимый код программы. Для ее исполнения достаточно набрать в командной строке:

>test.exe

16

После этого в окне сеанса MS-DOS появится результат:

From puts( ): Hello, World!

Казалось бы, все ясно, но перед начинающим еще долгий путь изучения и языка Си и возможностей компилятора. Одновременное освоение и нового языка программирования, и традиционных для него приемов обработки программы, предоставляемых транслятором, весьма сложно. Поэтому начнем с простейших средств языка и не будем пока отступать от описанной схемы трансляции и исполнения программ. Много интересного и неожиданного можно узнать, экспериментируя с нашей простой однофайловой программой, состоящей всего из одной функции main(), в которой всего два оператора – обращение к библиотечной функции puts() и завершение работы программы – оператор return.

Если вы проделали все описанные выше действия с программой

01_01.с и получили на экране "From puts( ): Hello, World!", то можно начинать эксперименты.

1.4. Модификации исходного текста программы

ЭКСПЕРИМЕНТ. Сделайте дубль программы 01_01.c и разместите его в файле 01_01_1.c. Измените текст в этом файле таким образом, чтобы программа была записана в одну длинную строку.

Вот ее начало (многоточием в книге отмечена незавершенность текста):

/* 01_01_1.c - */#include <stdio.h>int...

Запустите трансляцию, набрав в командной строке:

>gcc 01_01_1.c -о test.exe

Трансляция будет прервана (заметьте, на каком шаге), а в окне MS-DOS появится сообщение о характере обнаруженной ошибки:

01_01_1.c:1: '#include' expects "FILENAME" or <FILENAME>

17

(Перевод: Директива '#include' предусматривает использование конструкций: "имя_файла" или <имя_файла>).

Обратите внимание на формат диагностического сообщения:

имя_файла_с_программой: номер_строки: текст_сообщения

В том случае, когда сообщение относится к программе в целом, номер_строки отсутствует. В нашем случае ошибка обнаружена в первой (и единственной) строке.

ЭКСПЕРИМЕНТ. Разделите длинную строку текста программы 1_01_1.с на две строки таким образом:

/* 01_01_2.c - */#include <stdio.h> int main () { puts("From puts(): Hello, World!"); return 0;}

Запустите трансляцию. Опять то же сообщение об ошибке.

01_01_2.c:2: `#include' expects "FILENAME" or <FILENAME>

ЭКСПЕРИМЕНТ. По-другому разделим текст из 01_01_1.с на две строки:

/* 01_01_3.c – "в 2 строки" */#include <stdio.h>

int main () { puts("From puts(): Hello,World!");...

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

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

ЭКСПЕРИМЕНТ. Удалим из текста препроцессорную директиву #include <stdio.h> и запишем текст программы в одну строку, разместив его в файле 01_01_4.c (приводить ее текст здесь не станем).

18

Трансляция в компиляторе DJGPP пройдет успешно, программа правильно выполнится, т.е. на экран будет выведено:

From puts(): Hello, World!

Результат интересный и вызывающий недоумение. Зачем же нужна директива #include, если от нее одни ошибки? Попробуем разобраться.

1.5. Прототипы функций

ЗАДАЧА 01_02. Вывести на экран сообщение "The value of PI=3.14159", составив его из двух аргументов функции puts(): "The value of PI=" и "3.14159".

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

/* 01_02.c - неверное обращение к puts() */ int main ()

{

puts("The value of PI=", "3.14159"); return 0;

}

Выполните трансляцию:

>gcc 01_02.c -o test.exe

Трансляция пройдет без ошибок, но при исполнении программы второй аргумент функции puts() вовсе не будет учтен. Результатом станет фраза из первого аргумента:

The value of PI=

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

19

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

int puts(const char * _s);

К прототипам функций мы еще неоднократно вернемся, поэтому не станем пугаться незнакомых обозначений вида const char *. Вместо этого проведем еще один эксперимент.

ЭКСПЕРИМЕНТ. Дополним текст программы 01_2.c одной строкой – разместим прототип функции puts() перед строкой int main().

/* 01_02_1.c – прототип и неверный вызов

puts() */

int

puts(const char _s);

/*

02

*/

int main ()

/*

03

*/

{

 

/*

04

*/

puts("The value of PI=", "3.14159");/*

05

*/

return 0;

/*

06

*/

}

 

/*

07

*/

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

MS-DOS:

01_02_1.c: In function `main': (в функции `main':)

01_02_1.c:5: too many arguments to function `puts' (слишком много аргументов у функции `puts')

Первое сообщение относится к функции main() в целом. Во втором диагностическом сообщении указан номер строки (5), где в тексте программы размещен вызов функции puts().

Как следует из диагностических сообщений, компилятор, сравнивая прототип функции puts() и обращение к ней, "отказался" соз-

20