КулЛиб - Классная библиотека! Скачать книги бесплатно
Всего книг - 712812 томов
Объем библиотеки - 1401 Гб.
Всего авторов - 274559
Пользователей - 125076

Новое на форуме

Новое в блогах

Впечатления

Влад и мир про Шенгальц: Черные ножи (Альтернативная история)

Читать не интересно. Стиль написания - тягомотина и небывальщина. Как вы представляете 16 летнего пацана за 180, худого, болезненного, с больным сердцем, недоедающего, работающего по 12 часов в цеху по сборке танков, при этом имеющий силы вставать пораньше и заниматься спортом и тренировкой. Тут и здоровый человек сдохнет. Как всегда автор пишет о чём не имеет представление. Я лично общался с рабочим на заводе Свердлова, производившего

  подробнее ...

Рейтинг: 0 ( 0 за, 0 против).
Влад и мир про Владимиров: Ирландец 2 (Альтернативная история)

Написано хорошо. Но сама тема не моя. Становление мафиози! Не люблю ворьё. Вор на воре сидит и вором погоняет и о ворах книжки сочиняет! Любой вор всегда себя считает жертвой обстоятельств, мол не сам, а жизнь такая! А жизнь кругом такая, потому, что сам ты такой! С арифметикой у автора тоже всё печально, как и у ГГ. Простая задачка. Есть игроки, сдающие определённую сумму для участия в игре и получающие определённое количество фишек. Если в

  подробнее ...

Рейтинг: 0 ( 0 за, 0 против).
DXBCKT про Дамиров: Курсант: Назад в СССР (Детективная фантастика)

Месяца 3-4 назад прочел (а вернее прослушал в аудиоверсии) данную книгу - а руки (прокомментировать ее) все никак не доходили)) Ну а вот на выходных, появилось время - за сим, я наконец-таки сподобился это сделать))

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

В начале

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
DXBCKT про Стариков: Геополитика: Как это делается (Политика и дипломатия)

Вообще-то если честно, то я даже не собирался брать эту книгу... Однако - отсутствие иного выбора и низкая цена (после 3 или 4-го захода в книжный) все таки "сделали свое черное дело" и книга была куплена))

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

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
DXBCKT про Москаленко: Малой. Книга 3 (Боевая фантастика)

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

В общем герою (лишь формально вникающему в разные железки и нейросети)

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).

Изучаем Си [Александр Борисович Крупник] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

Содержание
Предисловие
Глава 1. Введение в программирование
Программы

1

Ячейки и типы

4

Программирование и трансляторы

6

Первая программа на Си

9

Глава 2. Первые шаги
Turbo C — среда программирования

12

Связь с внешним миром

16

Простые вычисления

20

В чем преимущество программ

22

Массивы

26

Как это делается

34

Глава 3. Странные типы
Странные типы-1

42

Странные типы-2

45

Анатомия типов

47

Подбирайте выражения

53

Строки и символы

57

Указатели

62

Глава 4. Действия
Очередность

65

ii

Условности

68

Работа с указателями

72

Битовые операции

74

Функции

80

Функции с длинными руками

84

Рекурсия или «раз, два, три»

87

Глава 5. Функции, указатели, массивы
Функции и массивы

96

Массивы и указатели

98

Указатели и массивы

100

Динамические массивы

102

Копирование строк

105

Глава 6. Файлы
Падение железного занавеса

109

Массивы указателей

114

Указатели на указатели

120

Файлы — не массивы!

123

Открытие файла

126

Глава 7. Строки
Считалочка

131

Сортировка строк

136

Указатель на функцию

138

Имена функций и указатели

144

Функция Qsort

146

Иголка, сено и лыко в строку

151

iii

Глава 8. Основные типы на сборочном
конвейере
Перечисления

157

Двухмерные массивы и указатели на…

163

Хранение и переработка двухмерных массивов

170

Записи

174

Записи и функции

178

Указатель на запись

182

Связанные записи

186

Typedef

191

Глава 9. Большие программы
Разделяй и властвуй

195

Extern или «Три поросенка»

199

Static

202

Глава 10. Макросы и переходы
Макросы

210

Управление текстом

215

Напутствие или GOTO

218

iv

Приложение А. Приоритеты
выполнения операторов

и

порядок
223

Что дальше?

224

Литература

225

Об авторе

226

v

Предисловие
Казалось бы, не так важно, какой язык программирования
учить первым. Зная один язык, легко выучить любой другой.
Но первый язык становится «родным» и для освоения других
понадобятся лишние усилия. Так зачем же начинать с
устаревшего Бейсика?
В этой книге делается попытка познакомить читателя с
программированием на примере языка профессиональных
программистов Си. Этот язык не только важен сам по себе, но
и открывает дорогу к другим современным и очень
популярным языкам, таким как C++, JAVA, PERL и JavaScript.
Язык Си считается трудным для изучения, и это отчасти так,
если его учить после Бейсика, Фортрана или даже Паскаля. Но
если Си станет первым изучаемым языком, все будет гораздо
проще.
Пусть читателя не пугает, что после короткого введения (Глава
1 «Введение в программирование»), мы сразу перейдем к
описанию среды программирования Turbo C (глава 2 «Первые
шаги» ) и к простейшим программам на Си. Такой порядок
изложения только облегчит его первые шаги. Уже в главе 3
«Странные типы» начинается знакомство с указателями —
трудной, но крайне важной для понимания языка темой. Это
знакомство углубляется на протяжении всей книги, и к ее
концу указатели должны стать родными читателю, как
становится родным язык, который окружал человека с детства.
Эта книга задумана и написана с целью рассказать пытливому
читателю о самых трудных, самых принципиальных
особенностях языка, что, конечно, не значит, что она не учит
практическому программированию на Си. Книга построена на
простых коротких примерах, которые помогут начать
программировать уже с первых ее страниц.
Мне хотелось, чтобы эта книга стала одной из первых в
длинном ряду других книг по программированию,
операционным системам и алгоритмам, которые нужно
прочитать, чтобы стать программистом-профессионалом.
Насколько исполнилось это желание — судить читателю.

vi

Александр Крупник
krupnik@sandy.ru
http://www.piter.com/display.phtml?a_id=19312&web_ok=all
Нижний Новгород, 16 июля 2001 года.

vii

Глава 1. Введение в
программирование
Программы
Чтобы научиться программировать, нужно понимать, как
работает компьютер. А для этого совсем не обязательно
изучать
прохождение
электрических
сигналов
по
микросхемам. Достаточно вообразить ряд пронумерованных
ячеек, в каждой из которых может храниться число или
команда.
Представим себе маленького чумазого мальчика (назовем его
процессором), который выполняет команды, хранящиеся в
последовательно расположенных ячейках. Чтобы начать
работу, мальчику необходимо знать, в какой ячейке хранится
первая команда.
Предположим, что первая команда всегда хранится в одной и
той же ячейке, например, под номером 37. Открывая ячейку
(вообразим, что это ящичек с дверцей), мальчик достает оттуда
лист бумаги, на котором записана команда: «поместить в
ячейку 1 число 2». Выполнив первую команду (написав на
клочке бумаги требуемое число и бросив его в первую ячейку),
мальчик переходит к ячейке 38, где записана следующая
команда: «поместить число 3 в ячейку 2». После ее
выполнения в ячейке 2 оказывается число 3. Далее мальчик
переходит к ячейке 39, где содержится команда посложнее:
«прибавить к содержимому первой ячейки число из второй и
поместить результат в ячейку номер 3». Чтобы выполнить эту
команду, мальчику придется открыть первую и вторую ячейки,
достать оттуда числа, сложить их и поместить результат (число
5) в третью ячейку. Выполнив эту команду, мальчик переходит
к следующей, 40-й ячейке, где записана команда «СТОП».
Увидев ее, он заканчивает работу и ждет. Команды и
результаты их выполнения показаны в табл. 1.1.

1

Таблица 1.1. Простейшая
выполнения на каждом шаге

Ячейка

программа

и

Команда

результат

ее

Ячейки
памяти
1-? 2-? 3-?

37

Поместить в ячейку 1 число 2

1-2 2-? 3-?

38

Поместить в ячейку 2 число 3

1-2 2-3 3-?

39

Сложить содержимое ячеек 1 и 2,
результат записать в ячейку 3

1-2 2-3 3-5

40

СТОП

Только что выполненная мальчиком последовательность
действий, называется программой. И хотя ее результат —
сумма двух чисел — смехотворен, в ней есть многое из того,
что необходимо знать
начинающему программисту.
Становится, например, понятно, что программы и данные
хранятся в одной и той же памяти (последовательности ячеек).
Чтобы их различать, нужно заранее знать, где начинается
программа и где можно хранить данные. Нельзя записать
число 2 в ячейку 38, потому что это испортило бы программу.
Программа выполняется последовательно по шагам. Выполнив
команду из 37-ой ячейки, мальчик переходит к следующей, 38й и так до тех пор, пока ему не встретится команда СТОП.
Правда, естественный порядок команд может нарушаться, как,
например, в программе, показанной в табл. 2, при следующем
исходном состоянии ячеек:
Ячейка

Содержимое

2

5

5

'П'

6

'Р'

7

'И'

2

8

'В'

9

'Е'

10

'Т'

11

'0'

Таблица 1.2. Вторая программа

Ячейка

Команда

37

Прочитать содержимое ячейки, адрес которой
хранится в ячейке 2

38

Если оно равно 0, перейти к ячейке 42

39

Напечатать содержимое ячейки, адрес которой
хранится в ячейке 2

40

Увеличить содержимое ячейки 2 на 1

41

Перейти к ячейке 37

42

СТОП

Самая трудная команда в этой программе, — конечно же,
первая. Она состоит из нескольких действий. Первое — чтение
содержимого ячейки 2. Прочитав число 5 из ячейки 2, мальчик
знает, что это совсем не та пятерка, которую он получил в
результате выполнения первой программы, а адрес некой
ячейки памяти. Такие адреса в языке Си называются
указателями. Второе действие уже проще: мальчик читает
содержимое ячейки 5 и переходит к следующей команде,
записанной в ячейке 38. Здесь проверяется, равно ли нулю
содержимое пятой ячейки. Если да, мальчик переходит к 42-й
ячейке и останавливается. Если нет, — рисует мелом на
грифельной доске букву, хранящуюся в пятой ячейке,
увеличивает адрес в ячейке 2 на единицу, и переходит к началу
программы, то есть к ячейке 37.

3

Ячейки и типы
В предыдущем разделе наш мальчик-процессор, когда ему
нужно было поместить в ячейку какое-то число, просто писал
его на бумажке и клал ее в соответствующий ящик. Как вы
понимаете, на самом деле все устроено иначе. Ячейки
реального компьютера не безразмерны, и в них можно
поместить числа, не превышающие, скажем, 256 или 65535.
Происходит это потому, что ячейка компьютера — не пустой
ящик, куда бросают бумажки, а набор из нескольких
переключателей, каждый из которых находится в одном из
двух состояний: верхнем (его часто обозначают единицей) или
нижнем (его обозначают нулем). В ячейке машины таких
переключателей, как правило, восемь (каждый переключатель
называют битом и говорят, что в ячейке 8 бит или 1 байт). То,
что мог бы увидеть мальчик, открыв дверцу ячейки, показано
на рис. 1.1

Рис. 1.1. Внутренности ячейки

Чтобы не рисовать каждый раз внутренности ячейки,
положение переключателей обозначают цифрами. Содержимое
ячейки, показанное на рис. 1.1, может быть представлено в
виде последовательности нулей и единиц:
1 0 0 0 1 1 0 0
Попробуем подсчитать, сколько разных чисел способна
вместить такая ячейка. Для этого вообразим ячейку, в которой
всего один переключатель c двумя состояниями: 1 и 0 (ВЕРХ и
НИЗ). Значит, в такой ячейке могут храниться два числа. Если
теперь добавить к ячейке еще один переключатель, число ее
состояний удвоится: теперь на каждое состояние первого
переключателя приходится два состояния второго. Значит, в
ячейке из двух переключателей всего 4 состояния, в ячейке из

4

трех переключателей — 8 (для каждого из четырех состояний,
в которых может находиться ячейка из двух переключателей,
есть два состояния третьего переключателя), а в ячейке из
восьми переключателей (битов) число возможных состояний
уже 2×2×2×2×2×2×2×2=256.
Значит, в ячейку из восьми бит можно записать числа от 0 до
255 (всего 256). Чтобы это сделать, можно, например,
пронумеровать все состояния ячейки и считать, что 57-е
состояние соответствует числу 57. Но проще воспользоваться
позиционным представлением чисел, с объяснения которого
начинается школьный курс информатики. Суть его в том, что
каждому биту приписывается определенный вес: крайний
правый бит это 1(20), второй справа бит — 2 (21),третий справа
— 4 (22), четвертый — 8 (23), пятый — 16 (24), шестой — 32
(25), седьмой — 64 (27), восьмой — 128 (28). Как видим, вес
каждого бита — это двойка, возведенная в степень, показатель
которой равен расстоянию от правого края числа. Первый
справа бит находится на нулевом расстоянии и поэтому его вес
равен 20. Вес второго справа бита равен 21 и т.д. Чтобы понять,
какое число записано в ячейке, нужно просуммировать
произведения весов на значения самих битов. Так, число
10000001 равно
1*28+0*27+0*26+0*25+0*240*23+0*22+0*21+1*20= 128+1=129,
а число 01010101 равно 64+16+4+1=85.
Когда в предыдущем разделе нашему мальчику нужно было
поместить в ячейку 1 число 2, он, открыв дверцу ячейки, видел
перед собой не просто ящик с клочком бумаги, а набор
переключателей, положение которых может быть любым
(говорят, что в этом случае ячейка содержит «мусор»). Задача
мальчика (поместить число 2 в ячейку 1) состоит в том, чтобы
установить второй справа переключатель в верхнее
положение, все же остальные — в нижнее. Это и будет
записью числа 2 в ячейку 1. В результате выполнения первой
программы в ячейке 3 окажется число пять, то есть 4+1:
00000101.
Теперь мы понимаем, что внутренности ячейки памяти
выглядят всегда как последовательность нулей и единиц,

5

независимо от того, что в них находится: число, буква или
адрес другой ячейки. Значит, для правильного выполнения
программы процессор должен знать, данные какого типа
хранятся в ячейках, и в соответствии с этим выполнять свою
работу.
Наш воображаемый мальчик узнавал о типе хранящихся в
ячейке данных из самой программы. Если он встречал команду
«поместить число 2 в ячейку 1», то устанавливал в верхнее
положение второй переключатель справа, остальные же
устанавливал в нижнее (или нулевое) положение. При
выполнении команды «поместить знак ‘2’, в ячейку 1» мальчик
поместит в ячейку число 50, то есть 00110010, потому что
знаки представляются в машине не так, как цифры.
Итак, в ячейках памяти есть только двоичные цифры, но в
зависимости от того, что (какой тип данных) хранится в
ячейке, эти цифры будут разными. С каждой ячейкой памяти
связан определенный тип.

Программирование и трансляторы
Не знаю, как вам, а мне уже становится жаль нашего мальчика
— ведь ему приходится бегать от ячейки к ячейке, читать
сначала команды, потом обращаться к ячейкам с данными,
снова переходить к командам. Возможно, малый рост не
позволяет ему добраться до всех ячеек, и ему приходится
двигать тяжелую приставную лестницу. И все это без
малейшего перерыва, пока не кончится программа.
Впрочем, жалость здесь неуместна — ведь никаких мальчиков,
на самом деле не существует. Вместо воображаемого мальчика
всю черную работу выполняет процессор — кремниевая
пластина размером с пару коробков, на которой расположены
миллионы транзисторов. За одну секунду процессор в
состоянии выполнить сотни миллионов простых операций:
записи/считывания чисел из ячеек памяти, сложений,
вычитаний и т.д. Процессор способен работать годы напролет,
ему не требуются выходные и перерывы на обед, и всю свою
жизнь он не расстается со своей «мамой» — материнской
платой (рис. 1.2).

6

Рис. 1.2. Процессор и его «мама»

Жалеть на самом деле нужно не того, кто бездумно исполняет
программы, а того, кто их пишет, то есть программистов. Ведь
им приходится выделять ячейки, где будут храниться данные и
программа. Им нужно помнить, что в первой ячейке записано
число 2, а во второй — 3. Им же необходимо заботиться о том,
чтобы данные не «налезли» на программу. А если это все-таки
произойдет, придется не только перемещать программу в
другую область памяти, но и менять в ней некоторые команды.
Вспомним, что во второй нашей программе была команда :
38 Если содержимое ячейки равно '0',
перейти к ячейке 42
Если предположить, что программа будет перенесена в другое
место памяти, придется менять эту команду, потому что
переход уже будет не к 42-й, а какой-то другой ячейке. Менять
программу придется и тогда, когда между 37-й и 42-й
ячейками будет вставлена дополнительная команда.
Иными словами, работа непосредственно с ячейками памяти и
командами процессора требует от программиста огромного
труда и чудовищного внимания. Программист не может дать
имена содержимому ячеек, например, назвать ячейку 3 из
первой нашей программы sum, чтобы показать, что в ней
будет записана сумма чисел. Ему очень трудно менять что-то в

7

программе, потому изменение программы в одном месте
неизбежно потребует изменений во многих ее местах, а это
значит, что программисту очень трудно будет избежать
ошибок. Первые, сейчас уже легендарные, программисты
хлебнули горя, ведь им приходилось не только распределять
вручную память, но и писать каждую команду в двоичном
коде — нулями и единицами! Им приходилось помнить, что в
ячейке 3 находится сумма, а, скажем, в ячейке 4 — начало
фразы. Им приходилось при поиске ошибок проверять
двоичные коды программ, и когда находилась ошибка, менять
сразу несколько команд, что в свою очередь приводило к
новым ошибкам.
Поэтому очень скоро сами программисты стали писать
программы иначе: вместо номеров ячеек они вводили обычные
имена, например, first (первый), second (второй), sum
(сумма), а сами команды процессора записывали не
двоичными цифрами, а обычными буквами. Программы из
последовательностей нулей и единиц превратились в тексты,
которые можно создавать в обычном редакторе. Такую
программу гораздо легче было понять и исправить
программисту, но ее уже не понимал процессор. Поэтому
перед исполнением программы, написанной на языке, удобном
для программистов, ее нужно было пропустить через
специальную служебную программу, так называемый
ассемблер, которая переводила машинные команды,
записанные буквами, в последовательности нулей и единиц,
понятные машине.
Ассемблеры
были
огромным
шагом
вперед
в
программировании, но они только иначе записывали команды
процессора. Ассемблерная программа (условные обозначения
команд и ячеек, применяемые программистами, называются
языком ассемблера) могла работать на компьютере только
одного типа. Исполнить ее на процессоре другого типа с
другой системой команд было невозможно.
Поэтому были изобретены так называемые языки высокого
уровня (такие, например, как BASIC или PASCAL), программы
на которых могут быть исполнены самыми разными
компьютерами. На самом деле, такие программы не могут
быть непосредственно выполнены ни на одном компьютере,

8

который, как мы это уже знаем, понимает только команды
собственного процессора. Поэтому программа, написанная на
языке высокого уровня, испытывает перед исполнением
несколько превращений. Сначала специальная программакомпилятор переводит ее в ассемблерный код, а затем уже
ассемблер превращает ее в последовательность машинных
команд. То есть теперь, вместо того, чтобы каждый раз писать
программу на машинном языке, нужно один раз помучиться и
написать на этом языке компилятор и ассемблер, а затем
писать программы на языке высокого уровня, что ускоряет
процесс программирования, делает его гораздо более
надежным и, что самое главное, написанные таким образом
программы, могут быть легко перенесены на другие
компьютеры, с другими командами процессора.
Прочитав эту книгу, вы научитесь писать программы на языке
Си — одном из самых популярных и мощных. Язык Си был
создан в начале 70-х годов прошлого века Деннисом Ритчи и
Кеном Томпсоном. К моменту создания языка Си эти
легендарные люди уже были опытными программистами, и
хорошо знали, каким должен быть новый язык. Быть может,
поэтому Си стал таким популярным. За тридцать лет язык Си
существенно изменился, но и сейчас многое в нем отражает
неповторимые личности создателей — их любовь к простоте и
краткости, их своеобразный юмор. Cи — язык
профессиональных программистов. Это значит, что освоить
его чуть труднее, чем Бейсик или Паскаль. Некоторые
конструкции языка Си неожиданны и требуют определенных
усилий для понимания. Но, приложив эти усилия, вы будете
щедро вознаграждены.

Первая программа на Си
Наша первая программа на Си, как и первая программа на
языке ячеек, с которой мы познакомились в начале этой главы,
складывает два числа (см. листинг 1.1).
Листинг 1.1

main() {

9

int first, second, sum;
first=2;
second=3;
sum=first+second;
}
Слово main, которое есть абсолютно в любой Си-программе,
показывает компилятору, откуда она начинается. Следом
всегда идут открывающая и закрывающая скобки (), внутри
которых могут быть дополнительные параметры, но сейчас
они нам ни к чему. Сама программа помещается между
фигурными скобками {}, которые также обязательны. Строчка
int first, second, sum;
командует компилятору выделить ячейки памяти для
переменных first, second и sum. Имена переменных в
языке Си начинаются обязательно с латинской буквы, далее
могут идти как буквы, так и цифры. Строчные буквы
отличаются от прописных, поэтому Sum и sum — разные
имена.
Словечко int перед именами переменных показывает, что в
выделенных ячейках памяти будут храниться целочисленные
переменные, которые могут быть только положительными и
отрицательными целыми и нулем. Три последующие строчки
first=2;
second=3;
sum=first+second;
в отличие от предыдущих, где только объявлялись переменные
и выделялась память для них, содержат инструкции, то есть
описания действий, которые должен выполнить компьютер.
Две строчки

first=2;
second=3;

10

засылают в переменные first и second нужные значения.
Целочисленным переменным можно присваивать и дробные
значения, например
second=3.5;
Результат (присвоенное значение переменной) все равно будет
целым, то есть переменная second окажется равной 3. Это
означает, что знак равенства в языке Cи используется не так,
как в алгебре. Запись second=3.5 означает в алгебре
равенство переменной second числу 3,5. Такая же запись в
языке Cи означает присваивание переменной second
значения 3.5. Как мы поняли, результат присваивания зависит
от типа переменной.
Для завершения знакомства с первой программой на языке Си
нам осталось только сказать о точке с запятой, которая
ставится в конце каждой инструкции. Именно точка с запятой
показывает компилятору, что текущая инструкция закончилась
и пора переходить к следующей. Это, между прочим, означает,
что инструкция может занимать несколько строчек. Например,
та же самая инструкция может выглядеть и так:
sum=
first
+
second;
Язык Си предоставляет программисту большую свободу и не
навязывает ему какой-то определенный стиль. Главное —
разумно пользоваться этой свободой и писать программы так,
чтобы их легко могли читать другие. Но об этом — в
следующих главах.

11

Глава 2. Первые шаги
Turbo C — среда программирования
А потом, когда вроде бы все затихло, я вдруг всеми своими
кодами почувствовал, что меня рассматривают.
Мерзопакостное это ощущение, когда ты сам сделать ничего
не можешь, а неизвестно кто, пользуясь этим, пытается
заглянуть за каждую твою команду, изучить заголовок. А
потом произошло что-то необыкновенное — Я БЫЛ
БРОШЕН В ЖИЗНЬ. Тот, кто меня рассматривал, вдруг,
обратившись молитвами к могучему ДОСу, схватил меня за
заголовок и вытащив из могилы забвенья , швырнул прямо в
мир процессора.
С.Расторгуев «Программные методы защиты информации в
компьютерах и сетях»
Наша первая программа на языке Си, с которой мы
познакомились в предыдущей главе (см. листинг 1.1), вышла
настолько простой, что ее безо всякого компьютера может
выполнить даже первоклассник.
Но бывают программы настолько сложные, что с ними не
справятся и миллионы первоклассников, даже если они будут
заниматься вычислениями все время, оставшееся от беганья с
бешеными криками по школьным коридорам.
Сложные программы может выполнить только компьютер. Как
мы уже говорили, программа, написаная на языке, удобном для
программиста, должна быть пропущены через компилятор,
преобразующий ее в последовательность команд процессора.
Из великого множества компиляторов, которые используются
на компьютере IBM PC, для изучения языка Си мы выберем
компилятор Turbo C 2.01 фирмы Borland. Эту программу, не
будь она классической, можно было бы назвать древней.
Turbo C 2.01 распространяется бесплатно, найти ее можно по

12

адресу
http://community.borland.com/article/images/20841/tc201.zip
Установка компилятора очень проста. Прежде всего нужно
распаковать
архивный
файл
tc201.zip.
Для
этого
(предварительно перейдя в папку, где хранится этот файл)
наберите в командной строке файловой оболочки1
pkunzip -d tc201.zip
и нажмите Enter.
После распаковки архива в той папке (директории), где он
находился, появятся три новых папки — DISK1, DISK2,
DISK3. Для установки компилятора можно просто переписать
содержимое папок DISK2 и DISK3 в папку DISK1 и затем
запустить находящуюся в ней программу install.exe. Далее
программа установки спросит вас название диска, с которого
устанавливается компилятор. В зависимости от того, на каком
логическом диске находится папка DISK1, нужно ввести одну
букву — ‘C’, ‘D’, ‘E’ или другую. Далее появится меню, в
котором нужно выбрать пункт «Install Turbo C on a hard
drive» (установить Turbo C на жесткий диск). Далее вам
покажут, куда будет установлен компилятор. Если, например,
компилятор устанавливается с диска E, то по умолчанию он
будет установлен в папку E:\TC. Можно выбрать и другое
место для компилятора. Для этого нажмите клавишу Enter,
введите новый путь и снова нажмите Enter. Теперь с помощью
стрелки ↓ выберите пункт Start Installation и еще раз нажмите
Enter. Компилятор установится в нужной папке. Остался
последний шаг: найти на диске С: файл AUTOEXEC.BAT и
вставить в него путь к компилятору. Если компилятор
установлен на диске E в папке TC, то путь к нему будет
выглядеть следующим образом:
PATH E:\TC;…
Внеся изменения в AUTOEXEC.BAT, не забудьте
перезагрузить компьютер. Теперь все готово к работе.
1
в системе DOS это будет, скорее всего, Norton Commander,
в системе Windows 95/98 лучше использовать FAR

13

Перейдите в папку, где находится исходный текст нашей
первой программы (пусть он хранится в файле l11.c), наберите
в командной строке файловой оболочки две буквы TC,
нажмите Enter и на экране монитора появится окно
компилятора. Чтобы загрузить в него исходный текст
программы, нажмите клавишу ‘F’, в появившемся меню
выберите пункт LOAD, далее выберите шаблон *.c и, наконец,
1
сам файл L11.c . В окне компилятора появится текст
программы (рис. 2.1).

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

Этот файл с исходным текстом программы можно создать в
редакторе Блокнот (Notepad). Программы нельзя писать в
редакторе MS Word — там другой формат, непонятный
компилятору.

14

текст программы. Действительно, программу можно
выполнять по шагам и при этом следить за состоянием любых
переменных.
Чтобы, например, следить за переменной first, подведем к
ней курсор (как на рис. 2.1), нажмем клавишу Ctrl и, не
отпуская ее, нажмем F7 (такая комбинация клавиш
обозначается как Ctrl-F7). В результате на экране появится
окно с именем переменной. При нажатии Enter переменная
окажется в нижней части окна Turbo C, под заголовком Watch
(рис. 2.2).

Рис. 2.2. Следим за переменными
Теперь программу можно выполнить по шагам: каждому
нажатию клавиши F8 соответствует один шаг. На рис. 2.2
показано состояние переменных после первого шага, на
котором переменной first было присвоено значение 2.
Как видим, переменные second и sum равны на этом шаге
888 и 27668, то есть содержат «мусор». На втором шаге
переменная second станет равной 3, а переменная sum попрежнему будет равна 27668. И лишь на третьем шаге sum
(убедитесь в этом сами!) станет равной 5, а переменные first

15

и second как были, так и останутся равны
соответственно.

2 и 3

Связь с внешним миром
Карло и виду не подал, что испугался, только ласково спросил:
— Деревянные глазки, почему вы так странно смотрите на
меня?
Но кукла молчала — должно быть потому, что у нее еще не
было рта.
А.Толстой, «Приключения Буратино»
Компиляторы, подобные Turbo C, как заботливые няньки
следят за каждым шагом программы (для этого существуют
клавиша F8 и окно Watch). Это необходимо, пока программа
не вышла из детского возраста и еще нуждается в опеке. Но
когда она взрослеет и уже хорошо справляется с поставленной
задачей, ей предстоит покинуть среду программирования и
начать самостоятельную жизнь. Программа должна работать
на любом компьютере — независимо от того, есть на нем
компилятор Turbo C или нет.
Хорошо знает об этом и компилятор, поэтому он создает
специальный файл с расширением .exe. Загляните в папку, где
хранится текст нашей первой программы (файл L11.c), и вы
увидите там еще два файла — L11.obj и L11.exe. Файл
L11.obj — вспомогательный, так называемый объектный
файл, создаваемый компилятором. Файл L11.exe и есть та
самая скомпилированная программа, которую можно
выполнить на любом компьютере, где есть системы DOS или
Windows. Чтобы выполнить программу, нужно перейти в
содержащую ее папку, набрать на клавиатуре имя L11 и
1
нажать Enter . Перешли в директорий? Набрали L11? Нажали
Enter? И что?

1

Так запускается программа в файловой оболочке Norton
Commander (для системы DOS) и FAR (для Windows 95/98).
Можно воспользоваться и Проводником (Explorer) в системе

16

А ничего. Молчание. И это понятно — ведь в нашей первой
программе нет никакой связи с внешним миром. Пока о
программе заботился компилятор, мы могли следить за тем,
что творится у нее внутри. Но когда программа попыталась
жить самостоятельно, выяснилось, что она еще не умеет
говорить — совсем как Буратино, которому папа Карло не
успел вырезать рот.
Чтобы заставить программу говорить, необходимо дополнить
ее функцией printf() и строкой #include 1
(см. листинг 2.1).
Листинг 2.1 Программа, которая умеет говорить

#include
main(){
int first, second, sum;
first=2;
second=3;
sum=first+second;
printf("Сумма=%d\n",sum);
}
Загрузив программу, показанную в листинге 2.1 в Turbo C и
скомпилировав ее простым нажатием клавиши F9, получим
exe-файл, запустив который увидим на экране компьютера:
Сумма=5
Значит, добавленная нами строчка
printf("Сумма=%d\n",sum);

Windows. Для этого перейдите в нужную папку и запустите
программу двойным щелчком мыши.
1

Об этой строке поговорим чуть позже

17

выводит на экран символы Сумма= и само значение
переменной sum (в нашем случае символ ‘5’).
С помощью printf() можно выводить на экран самые
разные сообщения, например, вывод слова «Мама» выглядит
следующим образом:
printf("МАМА");
причем курсор остается справа от последней буквы ‘А’. Чтобы
курсор оказался в начале следующей строчки, добавим в конец
фразы символ перевода строки '\n':
printf("МАМА\n");
Строка printf("Сумма=%d\n",sum)
в программе,
показанной в листинге 2.1, устроена чуть сложнее. Кроме букв
и управляющего символа '\n' в ней есть еще значок %d. Это
шаблон, который обозначает место выводимой на экран
переменной (переменная sum будет показана сразу за
символами «Сумма=»), причем это будет целое число (буква d
задает печать переменной целочисленного типа). Сама
переменная указывается правее и отделяется от других знаков,
заключенных в кавычки "…", запятой.
С помощью printf()можно выводить на экран значения
нескольких переменных. Для этого нужно сначала поместить в
кавычки символы и шаблоны для этих переменных, затем
поставить запятую и сами переменные, также разделенные
запятыми. Чтобы, например, вывести на экран значения
переменных first, second, sum, соответствующая строка
программы должна выглядеть так:
printf("first=%d second=%d sum=%d\n",
first, second, sum);
В результате мы получим на экране
first=2 second=3 sum=5
Функция printf(), с которой мы только что познакомились,
похожа на отдельную программу. На ее вход поступают
аргументы, разделенные запятыми. В функции
printf("МАМА\n");

18

всего один аргумент — последовательность символов,
заключенная в кавычки. В функции printf(), выводящей
три переменные, уже четыре аргумента, один из которых —
это строчка в кавычках, содержащая текст и шаблоны, а три
других — разделенные запятыми названия переменных.
Аргументы огораживаются в функции круглыми скобками ().
Если посмотреть в начало нашей программы, то станет ясно,
что main() — это тоже функция, которой можно передавать
аргументы, но о том, что это за аргументы и для чего их нужно
передавать, мы узнаем чуть позже.
А теперь вернемся к первой строчке листинга 2.1
#include
Этой строчки не было в нашей первой программе и легко
догадаться, что ее появление связано с функцией printf().
Дело в том, что в языке Си все переменные и функции должны
быть описаны, иначе компилятор не сможет правильно
перевести
программу
в
понятную
процессору
последовательность команд. До начала работы с переменными
компилятор должен знать их тип, иначе он не поймет, сколько
ячеек памяти выделять под переменную и как кодировать
хранящееся в ней двоичное число. То же самое относится и к
функциям. Чтобы правильно обращаться с функцией,
компилятор должен знать, какие аргументы ей могут
передаваться и как воспользоваться результатами ее работы.
Поэтому каждая функция должна быть описана еще до того,
как компилятор встретит ее в тексте программы. Если вы сами
создаете функцию (а в языке Си это вполне возможно), то сами
же должны ее описать. Но если функция стандартная, ее
описание хранится в специальном файле. Так, описание
printf() хранится в файле stdio.h. Этот файл —
неотъемлемая часть компилятора, который, встретив строчку
#include ,
находит файл stdio.h и вставляет его в текст программы
(именно туда, где стоит #include (включить), а уж потом
превращает программу, написанную на языке Си в
последовательность команд процессора.

19

Простые вычисления
Когда в Самоа были открыты начальные школы, туземцы
буквально помешались на арифметических вычислениях.
Отложив в сторону копья и луки, они расхаживали,
вооруженные грифельными досками и карандашами,
предлагая друг другу и гостям из Европы простейшие
арифметические задачи. Достопочтенный Фредерик Уолпол
сообщает, что его пребывание на прекрасном острове было
омрачено бесконечными умножениями и делениями.
Р. Бриффолт
Компьютер, как и туземцы с острова Самоа, умеет не только
складывать числа, но и вычитать, умножать, делить. Для
выполнения этих действий в языке Си используются обычные
знаки арифметических операций. Программа из листинга 2.2,
выполняет простые арифметические действия, а также
получает остаток от деления одного числа на другое.
Листинг 2.2

/* простые арифметические действия */
#include
main(){
int fst = 2, scnd = 3;
int df,prd,qt,rm;
df

= fst - scnd;

prd = fst * scnd;
qt
fst

= fst / scnd;
= 123;

scnd = 17;
rm = fst % scnd;
/* rm - остаток от деления fst на scnd */
printf("%d %d %d %d\n",df,prd,qt,rm);

20

}
Результат работы этой программы — четыре числа:
-1 6 0 4
Первое число (-1) получается в результате вычитания 3 из 2,
второе (6) — результат умножения 2 на 3, третье (0) — частное
от деления 2 на 31. Вообще-то, мы привыкли, что
2/3=0.6666…., но все наши переменные — целочисленные,
поэтому при присваивании результата деления переменной qt
автоматически берется целая часть результата, и поэтому
получается ноль. Четвертая цифра (4) есть остаток от деления
123/17. Легко проверить, что 123=17*7+4, то есть остаток
действительно равен четырем.
В листинге 2.2 кроме знаков новых арифметических действий
нам встретилась еще одна ранее незнакомая конструкция —
слова, обрамленные значками /*…*/. Это комментарии.
Компилятор игнорирует все символы, встреченные после
наклонной черты и идущей следом звездочкой. Делает он это
до тех пор, пока не встретит звездочку и идущую следом
косую черту.
Комментарии помогают понять, что делает программа.
Начинающие
программисты
часто
ленятся
писать
комментарии, не понимая, что программа быстро забывается и
уже через несколько месяцев никто, даже автор, не сможет
понять, что она делает.
Задача 2.1 Поиграйте в туземцев с острова Самоа.
Посмотрите, как работает программа из листинга 2.2 с другими
значениями переменных fst и scnd. Что получится, если
fst=1000, a scnd=800? Проверьте результат работы
программы с помощью калькулятора. Найдите значение
произведения fst*scnd, при котором программа еще выдает

1

Значения хранятся в переменных до тех пор, пока мы сами их
не поменяем. От того, что переменную fst прочитали в
инструкции df = fst - scnd, ее значение не меняется.
Читать переменную можно сколько угодно раз

21

правильный результат. Если это удалось,
догадаться, сколько бит в переменной типа int.

попробуйте

В чем преимущество программ
Огромной скоростью компьютеров можно воспользоваться
только лишь с помощью программ, в которых большое число
действий задается небольшим количеством строк.
Представим себе, что преподаватель информатики решил
«занять» класс на некоторое время и предложил написать
программу, которая складывает все числа от единицы до 100.
Эта программа может выглядеть так:
#include
main(){
int sum;
sum =0;
sum += 1; /* прибавить к sum единицу */
sum += 2; /* прибавить к sum двойку

*/

sum += 3; /* прибавить к sum тройку

*/

----------sum += 100; /*прибавить к sum сотню

*/

#printf("Сумма=%d/n", sum);
}
Каждая строка sum += i — это команда прибавить к
переменной sum число i. Эту команду можно записать и как
sum = sum + i1, но sum += i короче и понятнее.

1

Запись sum=sum+i означает, что к переменной sum
прибавляется переменная i и результат снова отправляется в
перемнную sum. То есть, ‘=’ означает в Си не равенство, как в
алгебре, а присваивание

22

Программу, набросок которой я только что показал, не так-то
просто написать, потому что она состоит более чем из 100
строк, ни одна из которых не повторяется. Но 100 строк, в
которых подсчитывается сумма, можно представить как
последовательность одинаковых групп команд:
#include
main(){
int sum,i;
sum = 0;
i = 0;
sum += i; /* прибавить к sum
i += 1;

/* прибавить к i единицу

i */
*/

------------/* повторить sum+=1; i+=1; еще 99 раз */
#printf("Сумма=%d/n", sum);
}
Такую программу уже проще написать, если использовать
возможности практически любого текстового редактора
копировать фрагменты текста1. Но представим себе, что нужно
будет сложить не 100, а тысячу или даже миллион чисел! Ясно,
что в программе должна быть конструкция, позволяющая
повторить одни и те же вычисления любое число раз. В языке
Си этому служит цикл while()(пока или до тех пор, пока).
Чтобы понять, как он работает, попробуем решить задачу

1

В Turbo C нужно сначала пометить блок текста: подвести
курсор к его началу, нажать Ctrl-K, затем B, подвести курсор к
концу блока, нажать Ctrl-K, затем K. После того как блок
помечен, подведите курсор к тому месту, куда нужно
скопировать блок и нажмите Ctrl-K, затем C.

23

попроще и напишем фрагмент программы, складывающей все
числа от 1 до 2:
sum=0;
i=1;
while (i max выполнится при i=7:
dig[7] равно 17, а текущее значение max — 11. Значит после
выполнения цикла printf() выведет на экран max=17, и
работа программы на этом завершится.
В программах, где заранее известно число оборотов цикла,
удобнее использовать другой его вид, задаваемый ключевым
словом for (для). Листинг 2.5 показывает, как применить
новый цикл for() к отысканию максимального числа в
массиве.
Листинг 2.5

#include
main(){
int dig[10]={5,3,2,4,6,7,11,17,0,13};
int max,i;
max=dig[0];
for(i=1;i max)
max=dig[i];
printf("max=%d\n",max);
}
Циклом for() управляют три выражения, разделенные
точкой с запятой. Первое (в нашем случае i=1) задает
начальное значение переменной, второе выражение задает
условие, при котором цикл выполняется (у нас это условие i
< =9). Условие, как и в цикле while(), проверяется до
выполнения каких-либо действий. Если оно не выполняется, о

29

цикл будет пропущен. Наконец, третье выражение задает
действие, выполняемое перед обращением к следующему
витку цикла. В нашем случае это увеличение переменной i на
1 (i++) после очередной проверки условия if(dig[i] >
max)…
Научившись определять наибольшее число в массиве, можно
решить следующую, очень распространенную задачу:
расставить элементы массива в порядке возрастания
(убывания). Эта задача называется сортировкой и очень
широко применяется на практике (вспомните, что фамилии в
телефонном справочнике, статьи в энциклопедическом словаре
стоят в алфавитном порядке, то есть отсортированы).
Сортировку массива чисел можно провести так, как мы
сделали бы это в обычной жизни: найти максимальный
элемент и поменять его местами с тем элементом, который
стоит в конце массива. Далее найти максимальный из
оставшихся элементов и поменять его с тем, что стоит на
втором месте от конца. И продолжать так до тех пор, пока не
будут отсортированы все элементы. Если, например, массив
состоит из десяти чисел и задается таблицей (напомним еще
раз, что нумерация элементов массива в языке Си начинается с
нуля)

0

1

2

3

4

5

6

7

8

9

a[0]

a[1]

a[2]

a[3]

a[4]

a[5]

a[6]

a[7]

a[8]

a[9]

5

3

2

4

6

7

11

17

0

13

то его максимальный элемент a[7] равен 17. Значит, на
первом этапе сортировки поменяются местами седьмой и
последний, девятый элемент массива:
5 3 2 4 6 7 11 13 0 |17
После обмена нужно найти максимальное значение в массиве
уже из девяти элементов: a[0]..a[8]. Шаги сортировки
показаны на рис. 2.3. Значения, которые будут меняться на

30

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

Рис. 2.3. Простейшая сортировка

Выполняющая сортировку программа показана в листинге 2.6.
Листинг 2.6

#include
main(){
int dig[10]={5,3,2,4,6,7,11,17,0,13};
int i,j,N,mm,tmp;
N=10;
for(i=N-1;i>=1;i--){
mm=0;
for(j=1;j dig[mm])
mm=j;

/* где max

*/

31

tmp=dig[i];

/* запомнить конец

*/

dig[i]=dig[mm]; /* максимум в хвост */
dig[mm]=tmp;/* конец - на место max */
}
for(i=0;i=1;i--){

}
Обратите внимание на то, что в цикл входят все инструкции,
обрамленные фигурными скобками {}. Если скобки убрать, то
будет выполнена только первая инструкция mm=0, которая
засылает в переменную mm, хранящую номер максимального
элемента, первоначальное значение 0.
В цикле
for(j=1;j dig[mm])
mm=j;

/* где max */

проверяется, действительно ли нулевой элемент —
максимальный. Если это не так, то положение максимального
элемента будет запомнено в переменной mm.
Когда максимальный из двух сравниваемых элементов найден,
нужно поменять его с элементом массива, номер которого
хранится в переменной i. Сначала i равна N-1 (то есть,
девяти), и максимальный элемент среди dig[0]…
dig[9]меняется с девятым элементом массива. Элементы
массива меняют местами три инструкции:

32

tmp=dig[i];

/* запомнить конец */

dig[i]=dig[mm]; /* максимум в хвост*/
dig[mm]=tmp;/* конец - на место max */
в которых для временного хранения dig[i] используется
переменная tmp (от английского temporary – временный).
Теперь нужно найти максимум среди оставшихся чисел
dig[0]…dig[N-2]. Это делается на втором витке цикла при
i=N-2. Найденный максимум помещается уже на второе
место с конца, то есть замещает восьмой элемент dig[8]. И
так продолжается до последнего оборота, где i=1. Здесь
просто сравниваются нулевой(не забывайте, что нумерация в
Си начинается с нуля!) и первый элементы. Если dig[0]
больше dig[1], они меняются местами. Если нет — массив
уже отсортирован.
Наконец, инструкция
for(i=0;i=1;i--){

for(j=0;j=1;i--){
max=dig[0];
for(j=0;j max){
max=dig[j];
mm=j;
}
}
tmp=dig[i];
dig[i]=max;
dig[mm]=tmp;
}
В цикле
for(j=0;j max){
max=dig[j];
mm=j;
}
вычисляется максимум, и в переменной mm запоминается
номер максимального элемента. Далее инструкции
tmp=dig[i];
dig[i]=max;
dig[mm]=tmp;
меняют максимальный из сравниваемых элементов и крайний
элемент массива, номер которого i.

36

Теперь можно снабдить ядро программы соответствующей
оболочкой: объявить переменные (задать их тип), написать
main(){}, — словом, сделать программу пригодной для
компиляции. То, что могло бы получиться, показано в
листинге 2.7
Листинг 2.7

#include
main(){
int dig[10]={5,3,2,4,6,7,11,17,0,13};
int i,j,max,N,mm;
N=10;
for(i=N-1;i>=1;i--){
max=dig[0]
for(j=0;j max){
max=dig[j]; /* максимум */
mm=j;

/* и его положение */

}
}
tmp=dig[i]; /* запомнить конец */
dig[i]=max; /* максимум в хвост
dig[mm]=tmp;/* конец - на место max */
}
for(i=0;i=1;i--){
max=dig[0];
for(j=0;j=1;i--){
max=dig[0];
mm=0;
for(j=0;j=1;i--){

40


потребует многократных исправлений при переходе к массиву
другого размера. Например, придется поменять цикл, в
котором отсортированные числа выводятся на экран:
for(i=0;i 10;
Чтобы ее расшифровать, нужно знать, что логические
операции выполняются слева направо1. Значит, результат
операции 10 > 101 (то есть 0) будет сравниваться с десятью:
i = 0 > 10;
В итоге i окажется равной нулю.
Кроме логического оператора > (больше) есть еще несколько
операторов с равным приоритетом, выполняемых слева
направо: >= (больше или равно), < (меньше), , >=, 1;
компилятор сначала займется выражениями 100 > 10 и 10 > 1
(оба выражения истинны, так что получатся две единицы), а
затем сравнит две единицы и запишет в переменную x
результат этого сравнения (тоже единицу).
У следующих двух логических операторов && и || еще более
низкий приоритет, потому что они имеют дело уже с
результатами сравнений. Исследовать эти самые логические из
всех разобранных логических операторов поможет программа,
показанная в листинге 4.2.
Листинг 4.2.

#include
main(){
int i,j;
for(i=0;i) и
влево ( x2) ? x1 : x2;
По сравнению с конструкцией, которую мы рассматривали
раньше
if(x1 > x2)
max=x1;
else
max=x2;
условное выражение более компактно, и хорошо смотрится
при записи в одну строчку.

95

Глава 5. Функции,
указатели, массивы
Функции и массивы
Мы уже научились передавать функции переменные (в том
случае, когда необходимо только читать их) и указатели (когда
нужно менять переменные, на которые настроен указатель). Но
как передать функции массив? Передать ей все элементы
массива или указатели на все элементы?
Ни то ни другое. Функции достаточно передать указатель на
нулевой элемент массива и число элементов в нем. Тогда она
сможет менять указатель в безопасных пределах и получит
полный доступ к любому элементу массива.
Посмотрим, например, как может выглядеть функция, которая
определяет максимальный элемент массива. Разумно
спроектировать ее так, чтобы она принимала указатель на
нулевой элемент массива и число элементов в нем, а
возвращала максимальное значение. Прототип функции будет
при этом таким:
int maxi(int *a, int n).
В листинге 5.1 показана программа, которая использует
функцию maxi() для нахождения максимального элемента
массива:
Листинг 5.1

#include
int maxi(int *, int);
int main(){
int dig[10]={5,3,2,4,6,7,11,17,0,13};
int m;
m=maxi(&dig[0],10);

96

printf("%d\n",m);
return 0;
}
int maxi(int *a,int n){
int i,max;
max=*a;
for(i=1;i max)
max=*(a+i);
return max;
}
В самом начале программы показан прототип функции, int
maxi(int *, int), сообщающий о том, что функция
принимает указатель на int и целое int, возвращает также
целое.
Возвращаемое функцией значение присваивается переменной
m: m=maxi(&dig[0],10). Как видим, функции передается
адрес нулевого элемента массива &dig[0] и число элементов
в нем (10). Попав внутрь функции, эти аргументы
превращаются в указатель a и целочисленную переменную n.
Далее переменной max присваивается значение нулевого
элемента массива: max=*a. Затем в цикле перебираются все
остальные элементы с номерами от 1 до 9. В строке
if(*(a+i)
>
max)сравнивается текущее значение
переменной max со значением элемента i (напомним, что
согласно правилам работы с указателями, a+i указывает на iй элемент массива, а раскрытие ссылки *(a+i) дает значение
этого элемента). Если текущий элемент массива больше max,
то он становится новым значением максимума: max=текущее
значение, то есть max=*(a+i). В конце концов, значение max
возвращается во внешний мир и выводится на экран.

97

Очень поучительно сравнить программу, показанную в
листинге 5.1 с программой из раздела «Массивы» второй
главы (листинг 2.4), также вычисляющей максимальное
значение в массиве, но без использования функций.
Программа, из листинга 2.4, как одноразовый стаканчик,
годится для решения единственной задачи: нахождения
максимального элемента массива dig[], состоящего из десяти
элементов. А программа, показанная в листинге 5.1 (вернее,
функция maxi()), уже способна вычислить максимальный
элемент в любом целочисленном массиве!
Задача 5.1 Подумайте, как можно сконструировать функцию,
которая возвращает не только максимальное значение, но и
номер наибольшего элемента в массиве.

Массивы и указатели
Выводя на экран содержимое массива (см. раздел «Строки и
символы» в главе 3), мы уже поняли, что функции printf()
можно вместо указателя на начало строки передать само имя
строки. Передача имени вместо указателя, оказывается,
возможна не только для строк, но и для любых массивов и
любых функций, принимающих указатели. Так, в программе из
листинга 5.1 можно было бы вместо
m=maxi(&dig[0],10)
написать просто m=maxi(dig,10).
Это значит, что компилятор рассматривает имя массива как
адрес его нулевого элемента. И он поступает с именем массива
так, как будто это указатель! Встретив какой-то элемент
массива, например arr[i], компилятор преобразует его по
правилам работы с указателями: *(arr + i). То есть, arr
— это как бы указатель, i — целочисленная переменная.
Сумма arr+i указывает на i-й элемент массива, а оператор
раскрытия ссылки * дает значение самого элемента. На
рис. 5.1 показан массив х, в котором хранятся три целых числа
— 13, 28 и 5, и некоторые комбинации имен элементов,
указателей и связанных с ними операторов & и *.

98

Рис. 5.1. Действия с именем массива и его элементами

Самая важная строчка на рисунке — предпоследняя. Она
показывает, что адрес нулевого элемента массива &(x[0])
равен имени массива x.
Преобразование x[0] в *x, показанное на рис. 5.1, может
навести на мысль, что имя массива — и есть указатель на его
нулевой элемент. Но это не так. Отличие имени массива от
указателя в том, что имя не может быть изменено. Пусть
имеется массив x[i]. Как мы теперь знаем, его нулевой
элемент равен *x, первый *(x+1) и т.д. Но если бы x был
настоящим указателем, его значение можно было бы

99

увеличить на единицу x=x+1 и тогда x указывал бы на первый
элемент массива. Ничего подобного с именем массива сделать
нельзя. Оно всегда указывает в одно и то же место — на свой
нулевой элемент.
Различие между указателями и массивами легче понять, если
сравнить два объявления строки, с которыми мы встретились в
главе 3 (разделы «Строки и символы» и «Указатели» главы 3):
сhar a[]="МАМА";
сhar *p="МАМА"; ,
Первая строка объявляет массив a[], в котором 5 элементов:
четыре буквы и завершающий символ '\0'. Адрес нулевого
элемента определяется во время компиляции, и в программе
нет такой переменной, где бы он хранился. Когда компилятор
видит в строке printf("%s",a) имя массива a, то
подставляет вместо него адрес начала строки.
Во втором случае адрес начала строки хранится в переменной p
(указателе). Эту строку можно вывести на экран так же, как и
первую, передав функции printf() значение указателя,
хранящееся в переменной p:
printf("%s",p).
Но в указатель p, как и в любую переменную, можно записать
новое значение и тогда p будет указывать на что-то другое.

Указатели и массивы
Если значение i-го элемента массива x[i] равно *(x + i),
то логично предположить, что всякий фрагмент программы
*(p + i), где p — указатель, а i — целочисленная
переменная, можно записать как p[i]. И это, к чести языка
Си, действительно так, потому что компилятор, встретив в
программе запись p[i] преобразует ее к виду *(p+i)
независимо от того, что такое p — массив или указатель.
Значит, в функции maxi(), (см. листинг 5.1) можно указатель
a в выражении *(a+i) заменить на a[i] и переписать всю
функцию следующим образом:

100

int maxi(int *a,int n){
int i,max;
max=a[0];
for(i=1;i max)
max=a[i];
return max;
}
Писать *(a+i) или же a[i]— дело вкуса, хотя те, кто
программировал на Бейсике, предпочтут, скорее всего, второй
способ.
Завершим этот раздел еще одним вариантом программы,
суммирующей все элементы массива (см. листинг 5.2). От
предыдущей версии (см. листинг 4.3) ее отличает прежде всего
строка int *p=dig, передающая адрес нулевого элемента
массива «настоящему» указателю p. Этот указатель p (в
отличие от имени массива) можно менять так, чтобы он
указывал последовательно на все элементы массива.
Листинг 5.2

#include
main(){
int dig[10]={5,3,2,4,6,7,11,17,0,13};
int sum=0,i;
int *p=dig;
for(i=0;inext ó NULL.
Функция display(), быть может, лучше всего дает
почувствовать красоту и лаконичность связанных списков.
Начав с переменной root, которая указывает на первую
запись в списке, display() высвечивает поле hero, затем
получает указатель на новую запись и так до тех пор, пока
указатель не станет нулевым. NULL, как мы уже говорили,
действительно равен нулю. Поэтому цикл do{} while()
перестанет выполняться и вывод на экран прекратится.
Задача 8.7. Можете ли вы сказать, не компилируя и не
запуская программу, показанную в листинге 8.15, что она
выведет на экран?
Задача 8.8. В программе из листинга 8.15 записи,
составляющие связанный список, создаются функцией
malloc(). Как освободить память, занимаемую связанным
списком? Имейте в виду, что если сначала освободить память,
занимаемую первой записью free(root), то освободить
следующую запись free(root->next)уже нельзя, потому

1

Присваивания a=b=c выполняются в Си справа налево, то
есть сначала значение c присваивается переменной b, а затем
значение b присваивается переменной a.

190

что Си не гарантирует, что указатель root->next сохранится
после освобождения root.
Связанные списки, о которых говорилось в этом разделе, могут
показаться искусственными и малополезными, потому что
затрудняют доступ к отдельному элементу. В массивах можно
прямо обратиться к элементу под номером десять, а в
связанных списках для этого нужно пройти девять
предыдущих. Зато в массивы очень трудно вставить новое
значение. Чтобы освободить место в массиве, нужно его
«раздвинуть», для чего придется переписать на новое место
весь его «хвост». Чтобы вставить новое значение в связанный
список, достаточно поменять пару указателей (рис. 8.4). То
есть, все как в жизни: идеального хранилища данных не
существует. В зависимости от задачи нужно выбирать либо
массив, либо список, либо что-то еще.

Рис. 8.4. Вставить элемент в связанный список легче, чем в
массив

Typedef
Слово typedef не создает новые типы, как, например, enum
или struct. Зато typedef позволяет по-другому назвать
типы уже существующие. И это хорошо — прежде всего
потому, что объявления переменных нужного типа становятся
короче. В предыдущем пункте объявлялись записи и указатели
на них. Получалось что-то очень длинное вроде struct
personage *repka (см. листинг 8.15). Чтобы создать

191

новый тип, у которого будет одно короткое имя, предварим
объявление записи словом typedef:
typedef struct personage {
char *hero;
struct personage *next;
} person;
Не будь слова typedef, эти строки создавали бы новую
запись person типа struct personage, но typedef
коренным образом меняет смысл объявления. Вместо записи,
создается новый тип person, такой же, как int или double.
С появлением слова typedef сама запись не создается. Это
нужно делать явно: строка
person *repka, *root
заменяет
struct personage *repka, *root;
и теперь вместо
repka = malloc(sizeof (struct personage *))
можно написать
repka = malloc (sizeof(person *));
Задача 8.9. Перепишите программу, показанную в листинге
8 15, объявляя новые типы с помощью typedef.
Естественно, typedef позволяет создавать и другие новые
типы. Чтобы понять, как это делается, полезно простое
правило: дописывание typedef слева от любого объявления
переменной, возвышает ее до названия нового типа. То есть,
конкретная переменная, имеющая имя и занимающая память,
после приписывания typedef превращается просто в имя
нового типа.
Пусть, например, в программе объявлена переменная типа
int:
int s32;

192

поставив слева typedef:
typedef int s32;
мы уничтожаем переменную s32 и превращаем ее в имя
нового типа, причем переменные нового типа будут такими
же, какой была s32 до приписывания слова typedef. Новым
типом можно пользоваться наравне с базовыми типами:
double, char и т.п:
s32 a=0, i,j;
Теперь a, i , j — простые переменные типа s32. Все они
такие же, какой была s32, то есть типа int.
Возьмем другое объявление:
int arr100[100];,
создающее массив из ста элементов типа int. Приписывание
слева слова typedef уничтожает сам массив arr100[100]
и создает новый тип arr100: массив из ста элементов типа
int. Теперь переменные этого типа можно объявлять и
использовать обычным образом:
typedef int arr100[100];
arr100 a;
int i;
for(i=0;i

Слева направо

< >=

Слева направо

== !=

Слева направо

&

Слева направо

^

Слева направо

|

Слева направо

&&

Слева направо

||

Слева направо

?:

Справа налево

= += -= *= /= %= &= ^= |= =

Справа налево

,

Слева направо
Приоритет унарных операторов +, - и * выше, чем
соответствующих бинарных.

223

Что дальше?
Здесь не делают домашних работ. Мы поможем
предложениями и советами, но сначала вы должны написать
что-то сами. Никто не хочет быть невежливым, но
большинство из нас считают, что делать чью-то домашнюю
работу — жестоко, потому что не дает правильно учиться.
Программированием, как и любым другим ремеслом надо
заниматься. Мы уже умеем делать домашние задания, по
крайней мере, не хуже вас и поэтому в таких занятиях не
нуждаемся.
Из конференции comp.lang.c.moderated
В Предисловии я написал, что хочу видеть свою книгу одной
из первых в длинном списке книг по программированию и
алгоритмам. И раз вы читаете эти строки, мое желание
сбылось. Хочу поздравить терпеливого читателя, который стал
пусть неопытным, пусть начинающим, но программистом.
Перед ним длинная и увлекательная дорога, предсказать все
многочисленные повороты которой невозможно.
Но хотелось бы пожелать, чтобы следующий шаг был связан с
«лучшей второй книгой всех времен и народов по языку Си»
— классической «Язык программирования Си» Брайана
Кернигана и Денниса Ритчи.
Если бы я начинал изучение языка Си сейчас, то лучшей (по
крайней мере, для меня) была бы моя собственная книжка. К
сожалению, мне, как и многим другим, приходилось учиться
по книге Кернигана и Ритчи. И надо честно сказать, что многое
в ней давалось мучительно, а кое-что (рекурсию, например) я
так тогда и не понял. Поэтому в своей книге я постарался
максимально подробно рассказать о самом трудном. Надеюсь,
что теперь чтение K&R (так во всем мире называют книгу
Кернигана и Ритчи) будет для вас не только поучительным, но
также легким и приятным.
Изучение Си, как и любого другого языка программирования,
немыслимо
без
упражнений.
Чтобы
научиться
программировать, нужно программировать. Задачи, которые

224

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

Литература
1. Б.Керниган, Д. Ритчи «Язык программирования Си».
Москва «Финансы и статистика», 1992
2. Ч.Уэзерелл «Этюды для программистов». М. Мир, 1982
3. Б.Керниган,
Ф.Плоджер
«Элементы
программирования», М. «Радио и связь», 1984
4. Ф.П.Брукс мл. «Как проектируются и
программные комплексы», М. «Наука», 1979

стиля
создаются

5. Ч. Петзолд «Программирование для Windows 95 в двух
томах», BHV, 1996

225

Об авторе
Крупник Александр
Борисович, родился
26 июня 1957 года в
г. Горьком. Окончил
Радиофизический
факультет
Горьковского
государственного
университета, канд.
ф.-мат. наук.

226