Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Турбо Си 2.0_TC01.doc
Скачиваний:
1
Добавлен:
01.04.2025
Размер:
2.68 Mб
Скачать

Глава 6

-------

ПРОГРАММИРОВАНИЕ НА ТУРБО СИ.

-----------------------------------------------------------------

Программировали ли вы на Си раньше? Вы могли слышать различ-

ные истории о том, как трудно изучать язык Си. Ерунда. Действи-

тельно, некоторые программисты ухитряются писать ужасные програм-

мы, которые трудно читать и отлаживать, но нет никакой

необходимости следовать их примеру. Основные элементы языка прог-

раммирования Си легки в понимании и использовании.

В этой главе...

-----------------------------------------------------------------

В этой главе мы обучим вас основным элементам языка Си и по-

кажем, как использовать их в ваших программах. В следующей главе,

"О некоторых особенностях программирования на Си", дается больше

информации о Си, в то время как в главе 12 "Углубленный курс

- 401,402 -

программирования на Турбо Си" рассказывается все о моделях памя-

ти, прерываниях, программировании на ассемблере и других трудно-

доступных вершинах.

Конечно, мы не сможем научить вас всему в программировании

на Си в одной или двух главах; написаны целые книги об этом.

(смотри библиографию в конце данного руководства).

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

главу 5 "Интегрированное окружение Турбо Си для разработки прог-

рамм", и научиться использовать меню и текстовой редактор Турбо

Си. Необходимо также установить Турбо Си (сделать рабочие копии

ваших дисков Турбо Си или скопировать файлы на ваш жесткий диск),

как это описано в главе 1.

Когда вы сделаете это, сядьте, включите вашу ПЭВМ (если она

еще не включена) и приговьтесь к изучению программирования в Тур-

бо Си.

Семь основных элементов программирования.

-----------------------------------------------------------------

Целью большинства программ является решение задач. Программа

решает задачи, манипулируя информацией или данными. Вы должны на-

учиться:

* вводить данные в программу

* выделять место для хранения данных

* задавать команды по обработке информации

* выводить информацию обратно из программы пользователю

(обычно, вам)

Вы можете организовать ваши команды так, что:

* некоторые из них будут выполняться только тогда, когда

специальное условие (или набор условий) истинно

* другие будут повторяться несколько раз

- 403,404 -

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

личных местах вашей программы.

Вот мы и описали семь основных элементов программирования:

ввод, тип данных, операции, вывод, условное выполнение, циклы и

подпрограммы. Этот список не полон, но он все же описывает те об-

щие элементы, которые обычно включают программы.

Большинство языков программирования содержат все эти элемен-

ты; многие из них, включая Си, имеют также и дополнительные воз-

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

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

затем двигаться дальше. Приведем обзорное описание каждого эле-

мента:

- вывод означает запись информации на экран, на диск или в

порт ввода-вывода;

- типы данных - это константы, переменные и структуры, кото-

рые содержат числа (целые и вещественные), текст (символы и

строки) или адреса (переменных и структур);

- операции присваивают одно значение другому, комбинируют

значения (складывают, делят и т.д.), и сравнивают значения

(равно, не равно и т.д.);

- ввод означает чтение данных с клавиатуры, с диска или из

порта ввода-вывода;

- условное выполнение относится к выполнению набора команд,

если заданное условие истинно (и пропуску их, если оно

ложно);

- циклы выполняют набор команд некоторое фиксированное коли-

чество раз или пока истинно некоторое условие;

- подпрограммы являются отдельно поименованными наборами ко-

манд, которые могут быть выполнены в любом месте программы

с помощью ссылки по имени;

Теперь мы рассмотрим, как использовать эти элементы в Турбо

Си.

- 405,406 -

Вывод

-----------------------------------------------------------------

Может показаться странным, что мы начинаем разговор именно с

вывода, однако программ которые ничего не выводят, почти нет. Под

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

(слова и картинки), на запоминающее устройство (гибкий или жест-

кий диск) или в порт ввода-вывода (последовательный порт, порт

принтера).

Функция printf

--------------

Мы уже использовали наиболее употребительную функцию вывода

в Си - подпрограмму printf. Ее целью является запись информации

на экран. Ее формат как прост, так и гибок:

printf(<строка формата>, <объект>, <объект>, ...);

Строка формата

--------------

Строка формата - это строка, которая начинается и заканчива-

ется двойными кавычками ("как эта"); цель printf - запись этой

строки на экран. Перед выводом printf заменяет все дополнительно

перечисленные объекты в строке в соответствии со спецификациями

формата, указанными в самой строке. Например, в последней прог-

рамме был следующий оператор printf:

printf("Сумма = %d \n",sum);

%d в строке формата - это спецификация формата. Все специфи-

кации формата начинаются с символа процента (%) и (обычно) сопро-

вождаются одной буквой, обозначающей тип данных и способ их пре-

образования.

Вы должны иметь для каждого объекта только одну соответству-

ющую ему спецификацию формата. Если объект имеет тип данных, не

соответствующий спецификации формата, то Турбо Си попытается вы-

полнить нужное преобразование.

Сами объекты могут быть переменными, константами, выражения-

- 407,408 -

ми, вызовами функций. Короче говоря, они могут быть чем угодно,

что дает соответствующее значение спецификации формата.

%d, используемое в спецификации, говорит о том, что ожидает-

ся некоторое целое число. Вот несколько других общеиспользуемых

спецификаций формата:

- %u целое число без знака

- %ld длинное целое число

- %p значение указателя

- %f число с плавающей точкой

- %e число с плавающей точкой в экспоненциальной форме

- %c cимвол

- %s строка

- %x или %X целое в шестнадцатиричном формате.

Вы можете задать ширину поля, помещая ее между % и буквой,

например, десятичное поле шириной 4 задается, как %4d. Значение

будет напечатано сдвинутым вправо (впереди пробелы), так что об-

щая ширина поля равна 4.

Если нужно напечатать знак %, то вставьте %%. \n в строке не

является спецификацией формата, а употребляется (по историческим

мотивам) как управляющая (escape) последовательность, и представ-

ляет специальный символ, вставляемый в строку. В этом случае \n

вставляет символ в начале новой строки, поэтому после вывода

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

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

найти в главе 11, из них наиболее часто используются:

- \f (перевод формата или очистка экрана)

- \t (табуляция)

- \b (забой <-)

- \xhhh (вставка символа c кодом ASCII hhh, где hhh

содержит от 1 до 3 16-ричных цифр)

Если вам нужно напечатать обратную косую черту, то вставьте

\\. При желании получить больше деталей о работе printf, обрати-

тесь к описанию printf в справочном руководстве Турбо Си.

Другие функции вывода: puts и putchar

-------------------------------------

- 409,410 -

Имеются две другие функции вывода, которые могут вас заинте-

ресовать: puts и putchar. Функция puts выводит строку на экран и

завершает вывод символом новой строки.

Например, можно переписать HELLO.C следующим образом:

#include <stdio.h>

main ()

{

puts("Hello, world");

}

Заметим, что в конце строки опущен \n; это не нужно, так как

puts сама добавляет этот символ.

Наоборот, функция putchar записывает единственный символ на

экран и не добавляет \n. Оператор putchar(ch) эквивалентен printf

("%c",ch).

Зачем же нужно использовать puts и/или putchar вместо

printf? Одним из доводов является тот, что программа, реализующая

printf, гораздо больше; если вы не нуждаетесь в ней (для числово-

го вывода или специального форматирования), то, используя puts и

putchar, можно сделать свою программу меньше и быстрее. Например,

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

puts, значительно меньше, чем файл .EXE для версии, использующей

printf.

- 411,412 -

Типы данных.

-----------------------------------------------------------------

При написании программы, вы работаете с некоторым видом ин-

формации, большинство которой попадает в один из 4-х основных ти-

пов: целые числа, числа с плавающей точкой, текст и указатели.

Целые - это числа, которые используются для счета ( 1, 2, 5,

-21 и 752, например).

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

экспоненту (5.4567*10^65). Иногда их называют действительными

(вещественными) числами (real).

Текст состоит из символов (а, Z, !, 3) и строк ("Это просто

проверка").

Указатели не хранят информацию; вместо этого каждый из них

содержит адрес памяти ЭВМ, в которой хранится информация.

Числа с плавающей точкой.

------------------------

Си поддерживает 4 основных типа данных в различных формах.

Вы уже использовали 2 из них: целые (int) и символы (char). Сей-

час будем модифицировать последнюю программу для использования

3-го типа: чисел с плавающей точкой (float). Войдите в редактор

Турбо Си и преобразуйте программу к следующему виду:

#include <stdio.h>

main()

{

int a,b;

float ratio;

printf("Введите два числа: ");

scanf("%d %d,&a,&b);

ratio = a / b;

printf("Отношение = %f \n", ratio);

}

Сохраните ее под именем RATIO.C, выйдя в главное меню и выб-

рав команду File/Write. Затем нажмите R для компиляции и выполне-

- 413,414 -

ния программы. Введя два значения (такие, как 10 и 3), вы получи-

те результат (3.000000).

Вероятно, вы ожидали ответа 3.333333; почему же ответ ока-

зался только 3? Потому, что a и b имеют тип int, отсюда и резуль-

тат тоже типа int. Он был преобразован к типу float при присваи-

вании его ratio, но преобразование имело место после, а не до

деления.

Вернитесь в редактор и измените тип a и b на float; измените

также строку формата "%d %d" в scanf на "%f %f". Сохраните прог-

рамму (нажмите F2), затем компилируйте и выполняйте. Результат

теперь 3.333333, как и ожидалось.

Имеется также две версии типа float, известная как double и

long double. Как вы могли догадаться, переменные типа double в

два раза больше переменных типа float, а переменные типа long

double еще больше. Это означает, что они могут иметь больше зна-

чащих цифр и больший диапазон значений экспоненты. Стандартные

размеры и диапазоны значений для этих типов в Турбо Си описаны в

Главе 11.

Три типа целых

--------------

В дополнение к типу int, Си поддерживает версии short int и

long int, обычно сокращаемые до short и long. Фактические размеры

short, int и long зависят от реализации; все, что гарантирует Си

- это то, что переменная типа short не будет больше (то есть не

займет больше байтов), чем переменная типа long. В Турбо Си эти

типы занимают 16 битов (short), 16 битов (int) и 32 бита (long).

Беззнаковые

-----------

Си позволяет вам объявлять некоторые типы (сhar, short, int,

long) беззнаковыми (unsigned). Это означает, что вместо отрица-

тельных значений эти типы имеют только неотрицательные (больше

или равные нулю). Переменные такого типа могут поэтому хранить

большие значения, чем знаковые типы. Например, в Турбо Си пере-

менная типа int может содержать значения от -32768 до 32767; пе-

ременная же типа insigned int может содержать значения от 0 до

- 415,416 -

65535. Обе занимают одно и тоже место в памяти (16 битов в данном

случае); но используют ее по-разному. Опять же, см. главу 11 для

выяснения деталей.

Определение строки

------------------

Си не поддерживает отдельный строковый тип данных, но он все

же предусматривает два слегка отличающихся подхода к определению

строк. Один состоит в использовании символьного массива, другой

заключается в использовании указателя на символ.

Использование символьного массива

- - - - - - - - - - - - - - - - -

Выбирете команду Load из меню файла и загрузите программу

HELLO.C снова в редактор. Теперь приведите ее к следующему виду:

#include <stdio.h>

#include <string.h>

main ()

{

char msg[30];

strcpy(msg, "Hello, world");

puts(msg);

}

[30], после msg, предписывает компилятору выделить память

для 29 символов, то есть для массива из 29 переменных типа char

(30-е знакоместо будет заполнено нулевым символом - \0 - часто

называемым нулевым завершителем или ограничителем ). Переменная

msg не содержит символьное значение; она хранит адрес (некоторого

места в памяти) первого из этих 29 переменных типа char.

Когда компилятор обнаруживает оператор strcpy(msg, "Hello,

world"), он делает две вещи:

- Создает строку "Hello, world", ограниченную (\0) символом

(с кодом ASCII 0),в некотором месте файла объектного кода.

- Вызывает подпрограмму strcpy, которая копирует символы из

- 417,418 -

этой строки по одному в участок памяти, указываемый пере-

менной msg. Он делает это до тех пор пока не будет скопи-

рован нулевой символ в конце строки "Hello, world".

Когда вы вызываете функцию puts(msg), то ей передается зна-

чение msg - адрес первой буквы, на которую он указывает. Затем

puts проверяет, не является ли символ по этому адресу нулевым.

Если да, то puts заканчивает работу; иначе puts печатает этот

символ, добавляет единицу (1) к адресу и делает проверку на нуле-

вой символ снова.

Из-за этой зависимости от нулевого символа известно, что

строки в Си называются "завершающиеся нулем", т.е. они представ-

ляют из себя последовательности символов, заканчивающиеся нулевым

символом. Этот подход позволяет снять ограничения с длины строк;

строка может быть такой длины, какой позволяет память для ее хра-

нения.

Использование указателя на символ

---------------------------------

Второй метод, который можно использовать для определения

строк, - это указатель на символы. Отредактируйте вашу программу

к следующему виду:

#include <stdio.h>

#include <string.h>

main()

{

char *msg;

msg = "Hello, world";

puts(msg);

}

Звездочка (*) впереди msg указывает компилятору, что msg яв-

ляется указателем на символ; другими словами, msg может хранить

адрес некоторого символа. Однако, при этом компилятор не выделяет

никакого пространства для размещения символов и не инициализирует

msg каким-либо конкретным значением.

Когда компилятор находит оператор msg = "Hello,world ", он

- 419,420 -

делает две вещи:

- Как и раньше, он создает строку "Hello, world\n", ограни-

ченную нулевым символом, где-то внутри файла объектного

кода.

- Присваивает начальный адрес этой строки - адрес символа H

переменной msg.

Команда puts(msg) работает так же, как и раньше, печатая

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

Имеется тесная зависимость между методами массива и указате-

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

Идентификаторы

--------------

До сих пор мы давали имена переменным, не заботясь о том,

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

эти ограничения.

Имена, которые вы даете константам, типам данных, переменным

и функциям называются идентификаторами. Некоторые из идентифика-

торов, мы использовали ранее - это:

char, int, float предопределенные типы данных

main главная функция программы

name,a,b,sum,msg,ratio переменные, определенные пользовате-

лем

scanf, printf, puts стандартные библиотечные функции

В Турбо Си имеется несколько правил об идентификаторах; вот

краткая их сводка:

- Все идентификаторы должны начинаться с буквы (a...z,

A...Z) или с подчеркивания (_).

- Остальная часть идентификатора может состоять из букв,

подчеркиваний и/или цифр (0...9). Никакие другие символы

не разрешены.

- Идентификаторы являются чувствительными от типа букв. Это

- 421,422 -

означает, что строчные буквы (a...z) - это не тоже самое,

что прописные буквы (A...Z). Например, идентификаторы

indx, Indx, INDX различны и отличаются друг от друга.

- Значимыми являются первые 32 символа идентификатора.

Операции

-----------------------------------------------------------------

Итак, вы научились получать данные в программу (и в ваши пе-

ременные); что же вы собираетесь делать с ними? Вероятно, как-то

с ними манипулировать, используя допустимые операции. Язык Си об-

ладает большим количеством таких операций.

Операция присваивания

---------------------

Самой общей операцией является присваивание, например, ratio

= a/b или ch = getch(). В Си присваивание обозначается одним зна-

ком равенства (=); при этом значение справа от знака равенства

присваивается переменной слева.

Можно применять также последовательные присваивания, напри-

мер: sum = a = b. В таких случаях присваивание производится спра-

ва налево, то есть b будет присвоено a, которая в свою очередь

будет присвоена sum, так что все три переменных получат одно и то

же значение (а именно, начальное значение b).

- 423,424 -

Одноместные и двуместные операции

---------------------------------

Си поддерживает обычный набор арифметических операций:

- умножение (*)

- деление (/)

- целочисленное деление (%)

- сложение (+)

- вычитание (-)

Турбо Си поддерживает одноместный минус (a + (-b)), который

выполняет двоичное дополнение как в расширении ANSI. Кроме этого,

Турбо Си поддерживает одноместный плюс (a + (+b)).

Операции приращения (++) и уменьшения (--)

------------------------------------------

В Си имеются некоторые специальные одноместные и двуместные

операции. Наиболее известными являются одноместные операции при-

ращения (++) и уменьшения (--). Они позволяют вам использовать

единственную операцию, которая добавляет 1 или вычитает 1 из лю-

бого значения; сложение и вычитание может быть выполнено в сере-

дине выражения, причем вы можете даже решить, сделать это до или

после вычисления выражения. Рассмотрим следующие строки програм-

мы:

sum = a + b++;

sum = a + ++b;

Первая означает: "сложить a и b, присвоить результат sum и

увеличить b на единицу".

Вторая означает: "увеличить b на единицу, сложить a и b, и

присвоить результат sum".

Это очень мощные операции, расширяющие возможности языка,

однако перед их использованием нужно убедиться, что вы хорошо по-

нимаете их действие. Измените SUM.C, как показано ниже, а затем,

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

- 425,426 -

#include <stdio.h>

main()

{

int a, b, sum;

char *format;

format = "a = %d b = %d sum = %d \n";

a = b = 5;

sum = a + b; printf(format,a,b,sum);

sum = a++ + b; printf(format,a,b,sum);

sum = ++a + b; printf(format,a,b,sum);

sum = --a + b; printf(format,a,b,sum);

sum = a-- + b; printf(format,a,b,sum);

sum = a + b; printf(format,a,b,sum);

}

Побитовые операции

------------------

Для обработки на уровне битов Си имеет следующие операции:

- сдвиг влево (<<)

- сдвиг вправо (>>)

- И (&)

- ИЛИ (|)

- исключающее ИЛИ (^)

- НЕ (~)

Они позволяют вам производить операции очень низкого уровня.

Для того, чтобы понять эффект этих операций, введите и выполните

следующую программу:

#include <stdio.h>

main()

{

int a, b, c;

char *format1, *format2;

format1 = " %04X %s %04X = %04X\n";

format2 = " %c%04X = %04X\n";

a = 0x0FF0; b = 0xFF00;

c = a<<4; printf(format1, a, "<<", 4, c);

- 427,428 -

c = a>>4; printf(format1, a, ">>", 4, c);

c = a & b; printf(format1, a, "& ", b, c);

c = a | b; printf(format1, a, "| ", b, c);

c = a ^ b; printf(format1, a, "^ ", b, c);

c = ~a; printf(format2, `~`, a, c);

c = -a; printf(format2, `-`, a, c);

}

Опять же, попробуйте предугадать то, что будет выводить эта

программа, не запуская ее. Заметим, что спецификаторы ширины поля

выравнивают выводимые значения; спецификатор %04X указывает на

использование нулей в начале числа, на ширину поля вывода четыре

цифры и на шестнадцатиричное представление (основание 16).

Комбинированные операции

------------------------

Си позволяет использовать некоторые сокращения при написании

выражений, содержащих многочисленные операции, описанные выше

(одноместные, двуместные, приращение, уменьшение и побитовые).

Так, любое выражение вида

<переменная> = <переменная> <операция> <выражение>;

может быть заменено на

<переменная> <операция> = <выражение>;

Ниже приводятся некоторые примеры таких выражений и способы

их сокращения:

a = a + b; сокращается до a += b;

a = a - b; сокращается до a -= b;

a = a * b; сокращается до a *= b;

a = a / b; сокращается до a /= b;

a = a % b; сокращается до a %= b;

a = a << b; сокращается до a <<= b;

a = a >> b; сокращается до a >>= b;

a = a & b; сокращается до a &= b;

a = a | b; сокращается до a |= b;

a = a ^ b; сокращается до a ^= b;

- 429,430 -

Адресные операции

-----------------

Си поддерживает две специальные адресные операции: операцию

определения адреса (&) и операцию обращения по адресу(*).

Операция & возвращает адрес данной переменной; если sum яв-

ляется переменной типа int, то &sum является адресом (расположе-

ния в памяти) этой переменной. С другой стороны, если msg являет-

ся указателем на тип char, то *msg является символом, на который

указывает msg. Введите следующую программу и посмотрите, что по-

лучится.

#include <stdio.h>

main()

{

int sum;

char *msg;

sum = 5 + 3;

msg = "Hello, thore\n";

printf(" sum = %d &sum = %p \n", sum, &sum);

printf("*msg = %c msg = %p \n", *msg, msg);

}

В первой строке печатается два значения: значение sum (8) и

адрес sum (назначаемый компилятором). Во второй строке также пе-

чатается два значения: символ, на который указывает msg (H), и

значение msg, которое является адресом этого символа (также наз-

начен компилятором).

- 431,432 -

Ввод

-----------------------------------------------------------------

В Си имеется несколько функций ввода; одни производят ввод

из файла или из входного потока, другие - с клавиатуры. Если тре-

буется детальная информация о функциях ввода Турбо Си, то читайте

о функциях ...scanf, read, а также Главу 8.

Функция scanf

-------------

Для интерактивного режима ввода, вероятно, можно использо-

вать в большинстве случаев функцию scanf. scanf это функция вво-

да, по смыслу эквивалентная printf; ее формат выглядит так:

scanf(<строка формата>,<адрес>,<адрес>,...)

В scanf используются многие из тех же спецификаторов формата

%<буква>, что и у функции printf: %d - для целых, %f - для чисел

с плавающей точкой, %s - для строк и т.д.

Однако scanf имеет одно очень важное отличие: объекты, сле-

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

В программе SUM.C содержится следующий вызов:

scanf("%d %d", &a, &b);

Этот вызов сообщает программе, что она должна ожидать от вас

ввода двух десятичных (целых) чисел, разделенных пробелом; первое

будет присвоено а, а второе b. Заметим, что здесь используется

операция адреса (&) для передачи адресов а и b функции scanf.

Белое поле

----------

Промежуток между двумя командами формата %d фактически озна-

чает больше, чем просто промежуток. Он означает, что вы можете

иметь любое количество "белых полей" между значениями.

Что такое белое поле? Это любая комбинация пробелов, табуля-

ций и символов новой строки. В большинстве ситуаций компиляторы и

программы Си обычно игнорируют белое поле.

- 433,434 -

Но что же надо делать, если вы хотите разделить числа запя-

той вместо пробела? Необходимо лишь изменить строку ввода:

scanf ("%d, %d", &a, &b);

Это позволяет вам ввести значения, разделенные запятой.

Передача адреса функции scanf

-----------------------------

Что если вы хотите ввести строку? Наберите и выполните

следующую программу:

#include <stdio.h>

main ()

{

char name[30];

printf("Как Вас зовут: ");

scanf("%s", name);

printf ("Привет, %s\n", name);

}

Поскольку name является массивом символов, значение name -

это адрес самого массива. По этой же причине перед именем name не

используется адресный оператор &, вы просто пишете scanf("%s",

name);

Обратите внимание, что мы использовали массив символов (char

name [30];), вместо указателя на символ (char *name;). Почему?

Причиной этого служит тот факт, что объявление массива на самом

деле резервирует память для хранения его элементов, а при объяв-

лении ссылки этого не происходит. Если бы мы использовали объяв-

ление char *name, тогда нам бы пришлось явным образом резервиро-

вать память для хранения переменной *name.

Использование gets и getch для ввода

------------------------------------

Использование scanf, однако, порождает другую проблему. Сно-

- 435,436 -

ва выполните программу, но теперь введите ваше имя и фамилию. За-

метьте, что программа использует в своем ответе только имя. Поче-

му? Потому, что введенный вами после имени пробел сигнализирует

scanf о конце вводимой строки.

Возможны два способа решения этой проблемы. Вот первый из

них:

#include <stdio.h>

main ()

{

char first[20],middle[20],last[20];

printf("Как Вас зовут:");

scanf("%s %s %s",first,middle,last);

printf("Дорогой %s, или вам приятней %s?\n", last,

first);

}

Это означает, что имеется три компоненты имени; в примере

функция scanf не пойдет дальше, пока вы действительно не введете

три строки. Но что, если необходимо прочитать полное имя, как од-

ну строку, включая пробелы? Вот второй способ решения:

#include <stdio.h>

main ()

{

char name [60];

printf("Как вас зовут: ");

gets (name);

printf ("Привет, %s\n", name);

}

Функция gets читает все, что вы набираете до тех пор, пока

не нажмете Ввод. Она не помещает Ввод в строку; однако в конец

строки добавляет нулевой символ (\0).

Наконец, есть еще функция getch. Она читает единственный

символ с клавиатуры, не выдавая его на экран (в отличии от scanf

и gets). Заметим, что у нее нет параметра ch; getch является

функцией типа char, ее значение может быть непосредственно прис-

- 437,438 -

воено ch.

Условные операторы

-----------------------------------------------------------------

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

условные и логические операторы. При этом возникают некоторые

непростые моменты в выражениях, которые мы приберегли для обсуж-

дения условных (true или false - истина или ложь) операторов.

Операции сравнения

------------------

Операции сравнения позволяют сравнивать два значения, полу-

чая результат в зависимости от того, дает ли сравнение истину или

ложь. Если сравнение дает ложь, то результирующее значение равно

нулю, если значение истинно, то результат равен 1. Ниже приведен

список операций сравнения:

> больше

>= больше или равно

< меньше

<= меньше или равно

== равно

!= не равно

Почему нас должно заботить, является ли нечто истиной или

ложью? Загрузите и выполните программу RATIO.C и посмотрите, что

произойдет, когда вы введете 0 для второго значения. Программа

напечатает сообщение об ошибке (Divide by zero - Деление на ноль)

и остановится. Теперь сделайте следующие изменения в программе и

запустите ее снова.

#include <stdio.h>

main ()

{

float a,b,ratio;

printf("Введите два числа: ");

- 439,440 -

scanf("%f %f",&a,&b);

if (b == 0.0)

printf("Отношение не определено\n");

else {

ratio= a / b;

printf("Отношение = %f \n",ratio);

}

}

Оператор, находящийся в двух следующих за оператором scanf

строках, известен как условный оператор if. Вы можете понимать

его так:"Если значение выражения (b == 0.0) истинно, сразу выз-

вать printf. Если значение выражения ложно, присвоить a/b пере-

менной ratio, затем вызвать printf."

Теперь, если вы введете 0 в качестве второго значения, то

ваша программа напечатает сообщение

Отношение не определено

и будет ожидать от вас нажатия любой клавиши для возврата в Турбо

Си. Если второе значение - ненулевое, то программа вычисляет и

печатает ratio, а затем ожидает нажатия клавиши - и все это бла-

годаря магии оператора if.

Логические операции

-----------------------------------------------------------------

Имеется также три логические операции: И (&&), ИЛИ (||) и НЕ

(!). Их не следует путать с описанными выше битовыми операциями

(&,|,~). Логические операции работают с логическими значениями

(истина или ложь) и позволяют составлять логические выражения.

Как же их отличать от соответствующих битовых операций?

- Эти логические операторы всегда дают в результате значение

либо 0 (ложь), либо 1 (истина), в то время как поразрядные опера-

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

до тех пор, пока не станет ясен результат.

- Логические операторы && и !! известны как операторы типа

"short circuit". Выполнение операторов такого типа прекращается

- 441,442 -

как только становится ясно, будет ли результат иметь значение ис-

тина или ложь. Предположим, что вы имеете выражение вида:

exp1 && exp2

Если exp1 - ложь, значит и все выражение ложь. Таким обра-

зом, exp2 никогда не будет вычисляться. Аналогично, если мы имеем

выражение вида

exp1 !! exp2

то exp2 никогда не будет вычисляться, если exp1 верно.

Дополнительные сведения о выражениях.

-----------------------------------------------------------------

Прежде чем перейти к обсуждению операторов цикла, мы дадим

некоторые комментарии к использованию выражений. Такие выражения,

как (b == 0.0) и (a <= q*r) довольно привлекательны по своей кра-

соте. Однако Си допускает написание более сложных и запутанных

конструкций, чем эти.

Операторы присваивания.

-----------------------------------------------------------------

Любой оператор присваивания, заключенный в круглые скобки,

является выражением с определенным значением, которое получается

в реэультате этого присваивания.

Например, выражение (sum = 5+3) имеет значение 8, поэтому

выражение ((sum = 5+3) <= 10) будет всегда иметь значение "исти-

на" (т.к. 8 <= 10). Более экзотичен следующий пример:

if ((ch=getch()) == 'q')

puts("До свидания! Программа завершена.\n");

else

puts("Продолжаем работу!\n");

Как работает эта конструкция? Если в программе используется

выражение ((ch=getch()) == 'q'), то она, дойдя до него, останав-

- 443,444 -

ливается и переходит в состояние ожидания ввода символа с клавиа-

туры. После того, как вы введете символ, осуществляется присваи-

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

введенного символа с символом 'q'. Если введенный символ равен

'q', то на экран будет выведено сообщение "До свидания! Программа

завершена.", в противном случае будет выведено сообщение "Продол-

жаем работу!".

Оператор запятая.

-----------------------------------------------------------------

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

множественных выражений, расположенных внутри круглых скобок. Вы-

ражение внутри скобок вычисляется слева направо и все выражение

принимает значение, которое было вычислено последним. Например,

если oldch и ch имеют тип char, то выражение

(oldch = ch, ch = getch())

присваивает переменной oldch значение ch, затем считывает символ,

вводимый с клавиатуры, и запоминает его в ch. Результатом всего

выражения, в итоге, будет значение введенного с клавиатуры симво-

ла. Приведем еще один пример:

ch='a';

if((oldch = ch, ch = 'b') == 'a')

puts("Это символ 'a'\n");

else

puts("Это символ 'b'\n");

Как вы считаете, какое сообщение будет выведено на экран вашего

дисплея в результате выполнения приведенной выше программы?

Оператор if.

-----------------------------------------------------------------

Обратимся теперь опять к оператору if, который фигурировал

при рассмотрении первых примеров. Оператор if имеет следующий ос-

новной формат:

- 445,446 -

if (значение)

оператор1;

else

оператор2;

где "значение" является любым выражением, которое приводится или

может быть приведено к целочисленному значению. Если "значение"

отлично от нуля ("истина"), то выполняется "оператор1", в против-

ном случае выполняется "оператор2".

Дадим пояснение относительно двух важных моментов по исполь-

зованию оператора if-else.

Во-первых, часть "else оператор2" является необязательной

частью оператора if; другими словами, правомерно употребление

следующей формы оператора if:

if (значение)

оператор1;

В этой конструкции "оператор1" выполняется тогда и только

тогда, когда "значение" отлично от нуля. Если "значение" равно

нулю, "оператор1" пропускается и программа продолжает выполняться

дальше.

Во-вторых, что делать если вы хотите выполнить более одного

оператора в зависимости от того ложно или истинно выражение, ука-

занное в операторе if? Ответ: используйте составной оператор.

Составной оператор состоит из:

- левой или открывающей фигурной скобки ({)

- последовательности операторов, разделенных между собой точ-

кой с запятой (;)

- правой или закрывающей фигурной скобки (})

В приведенном ниже примере в предложении if используется

один оператор

if (b == 0.0)

printf("Отношение не определено\n");

а в предложении else - составной оператор

- 447,448 -

else {

ratio = a/b;

printf( "Значение отношения равно %f\n", ratio);

}

Вы можете так же заметить, что тело вашей программы (функции

main) является подобием составного оператора.

Циклические конструкции в программах.

-----------------------------------------------------------------

Наряду с операторами (или группами операторов), которые мо-

гут выполняться в зависимости от каких-либо условий, существуют

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

и той же последовательности. Такой вид конструкции в программе

известен как цикл. Есть три основных типа циклов (хотя два из них

можно рассматривать как разновидность одного). Это цикл while

("пока"), цикл for ("для") и цикл do...while ("делать ... пока").

Рассмотрим их по порядку.

Цикл while.

-----------------------------------------------------------------

Цикл while является наиболее общим и может использоваться

вместо двух других типов циклических конструкций. В принципе мож-

но сказать, что по-настоящему для программирования необходим

только цикл while, а другие типы циклических конструкций служат

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

- 449,450 -

Загрузите с диска файл HELLO.C и измените его следующим об-

разом:

#inсlude <stdio.h>

main()

{

int len;

len=0;

puts("Наберите предложение, затем нажмите <Ввод>");

while ( getchar() != '\n')

{

len++;

}

printf("\nВаше предложение имеет длину %d символов\n",len);

}

Эта программа позволяет ввести предложение с клавиатуры и

подсчитать при этом, сколько раз вы нажали на клавиши клавиатуры

до тех пор, пока не нажали на клавишу <Ввод> (соответствует спе-

циальному символу конца строки - '\n'). Затем программа сообщит

вам сколько символов (символ '\n' не подсчитывается) вы ввели.

Оператор while имеет следующий формат:

while (выражение)

оператор

где "выражение" принимает нулевое или отличное от нуля значение,

а "оператор" может представлять собой как один оператор, так и

составной оператор.

В процессе выполнения цикла while вычисляется значение "вы-

ражения". Если оно истинно, то "оператор", следующий за ключевым

словом while, выполняется и "выражение" вычисляется снова. Если

"выражение" ложно, то цикл while завершается и программа продол-

жает выполняться дальше.

Обратите внимание на другой пример цикла while, который

также находится в файле HELLO.C:

#include <stdio.h>

- 451,452 -

main()

{

char *msg;

int indx;

msg = "Здравствуй, мир";

indx = 1 ;

while (indx <= 10 ) {

printf("Время # %2d: %s\n", indx,msg);

indx++;

}

}

После компиляции и выполнения этой программы на экране будут

отображены строки со следующей информацией:

Время # 1 : Здравствуй, мир

Время # 2 : Здравствуй, мир

Время # 3 : Здравствуй, мир

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

Время # 9 : Здравствуй, мир

Время # 10 : Здравствуй, мир

Очевидно, что оператор printf был выполнен ровно десять раз.

При этом значение параметра цикла indx изменилось от 1 до 10.

Немного подумав, вы сможете переписать этот цикл несколько

компактнее:

indx = 0 ;

while (indx++ < 10 )

printf("Время #%2d: %s\n",indx,msg);

Изучайте этот второй пример цикла while до тех пор, пока вам

не станет ясно, почему он работает точно так же, как и в первом

случае. Затем переходите к изучению цикла типа for.

Цикл for.

-----------------------------------------------------------------

- 453,454 -

Цикл for является одним из основных видов циклов, которые

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

Си. Однако, версия цикла for, используемая в Си, как вы увидите,

обладает большей мощностью и гибкостью.

Основная идея, заложенная в его функционирование, заключает-

ся в том, что операторы, находящиеся внутри цикла, выполняются

фиксированное число раз, в то время как переменная цикла (извест-

ная еще как индексная переменная) пробегает определенный ряд зна-

чений. Например, модифицируем программу, о которой говорилось вы-

ше, в следующую:

#include <stdio.h>

main()

{

char *msg;

int indx;

msg = "Здравствуй, мир";

for (indx = 1; indx <= 10; indx++ )

printf("Время #%2d: %s\n",indx,msg);

}

Выполните эту программу и вы убедитесь, что она делает те же

действия, что и программа с циклом while, которую мы уже разобра-

ли, и является точным эквивалентом первого ее варианта.

Теперь приведем основной формат цикла for:

for (выр1; выр2; выр3)

оператор

Так же, как и в цикле while, "оператор" в теле цикла for

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

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

({...}).

Заметим, что параметры цикла for, заключенные в скобки,

должны разделяться точкой с запятой (позиционный параметр), кото-

рая делит в свою очередь пространство внутри скобок на три секто-

ра. Каждый параметр, занимающий определенную позицию, означает

следующее:

- 455,456 -

- выр1 - обычно задает начальное значение индексной

переменной;

- выр2 - условие продолжения цикла;

- выр3 - обычно задает некоторую модификацию (приращение)

индексной переменной за каждое выполнение цикла.

Основной вариант цикла for эквивалентен следующей конструк-

ции, выраженной с помощью цикла while:

выр1;

while (выр2) {

оператор;

выр3;

}

Вы можете опускать одно, несколько или даже все выражения в

операторе for, однако о необходимости наличия точек с запятой вы

должны помнить всегда. Если вы опустите "выр2", то это будет рав-

носильно тому, что значение выражения "выр2" всегда будет иметь

значение 1 (истина) и цикл никогда не завершится (такие циклы из-

вестны еще как бесконечные).

Во многих случаях вам поможет использование оператора запя-

тая (,), который позволяет вводить составные выражения в оператор

цикла for. Вот, например, еще одна правильная модификация файла

HELLO.C с использованием составного выражения в операторе for:

#include <stdio.h>

main()

{

char *msg;

int up,down;

msg = "Здравствуй, мир";

for (up = 1, down = 9; up <= 10; up++, down--)

printf("%s: %2d растет, %2d уменьшается \n",msg,up,down);

}

Заметьте, что и первое и последнее выражение в этом цикле

for состоит из двух выражений, инициализирующих и модифицирующих

переменные up и down. Вы можете сделать эти выражения сколь угод-

- 457,458 -

но сложными. (Возможно вы слышали о легендарных хаккерах Си

(hacker - программист, способный писать программы без предвари-

тельной разработки спецификаций и оперативно вносить исправления

в работающие программы, не имеющие документации), которые запихи-

вают большинство своих программ в три выражения оператора for,

оставляя в теле цикла лишь несколько операторов).

Цикл do...while.

-----------------------------------------------------------------

Последним видом цикла является цикл do...while. Модифицируй-

те RATIO.C следующим образом:

#include <stdio.h>

#include <conio.h>

main()

{

float a,b,ratio;

do {

printf("\nВведите два числа: ");

scanf("%f %f", &a, &b);

if (b == 0.0)

printf("\n Деление на ноль!");

else {

ratio = a/b;

printf("\nРезультат деления двух чисел: %f",ratio);

}

printf("\nНажми 'q' для выхода или любую клавишу для"

" продолжения")

} while ( getch() != 'q');

}

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

другое.Затем порсит вас нажать любую клавишу. Если вы нажмете

клавишу 'q', то выражение в операторе цикла while в конце прог-

раммы примет значение "ложь" и цикл (а значит и программа) завер-

шится. Если вы введете какой-либо другой символ, отличный от 'q',

то выражение будет иметь значение "истина" и цикл повторится.

Формат цикла do...while можно представить в виде:

- 459,460 -

do оператор while (выр);

Основным отличием между циклом while и циклом do...while в

том, что операторы внутри do...while всегда выполняются хотя бы

один раз (т.к. проверка условия выполнения цикла осуществляется

после выполнения последовательности операторов, составляющих тело

цикла). Это похоже на цикл repeat...until в Паскале с одним, од-

нако, различием: цикл repeat выполняется дотехпор, пока его усло-

вие не станет истинным; а цикл do...while выполняется пока исти-

на.

Функции.

-----------------------------------------------------------------

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

итеративные операторы. Сейчас настало время задать нам вопрос: а

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

операторов, использующие различные наборы данных или находящиеся

в различных частях программы? Ответ прост: объединяйте эти группы

операторов в подпрограммы, к которым вы будете обращаться по не-

обходимости.

В Си все подпрограммы рассматриваются как функции. Теорети-

чески, каждая функция возвращает некоторое значение. Практически

же, значения, возвращаемые большинством функций игнорируются и

целое семейство новых определений языка Си (включая проект стан-

дарта Си, предложенный ANSI, и Турбо Си) позволяют описывать и

использовать в языке функции типа void, которые никогда не возв-

ращают значений. Никогда.

В Си вы можете и описывать и определять функцию. Когда вы

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

главный модудь main) информацию о том, каким образом должно осу-

- 461,462 -

ществляться обращение к этой функции. Когда вы определяете функ-

цию, вы присваиваете ей имя, по которому к ней будет осущест-

вляться обращение, и указываете, какие конкретно действия она

будет выполнять. Внимательно изучите этот модифицированный вари-

ант RATIO.C:

#include <stdio.h>

#include <conio.h>

/* Описание функций */

void get_parms(float *p1, float *p2);

float get_ratio(float divident, float divisor);

void put_ratio (float quotient);

const float INFINITY = 3.4E+38;

/* Главная (main) функция: стартовая точка программы */

main()

{

float a,b,ratio;

do {

get_parms(&a,&b); /* ввод параметров */

ratio = get_ratio(a,b); /* вычисление частного */

put_ratio(ratio); /* печать выходного результата */

printf("Нажми 'q' для выхода или любую клавишу для"

" продолжения\n");

} while ( getch() != 'q');

}

/* конец main */

/* Определение функций */

void get_parms(float *p1, float *p2)

{

printf("Введите два числа: ");

scanf("%f %f", p1, p2);

}

float get_ratio(float divident, float divisor)

{

if (divisor == 0.0)

- 463,464 -

return (INFINITY);

else

return(divident / divisor);

}

void put_ratio(float ratio)

{

if (ratio == INFINITY)

printf("Внимание! Деление на ноль!\n");

else

printf("Результат деления двух чисел: %f\n",ratio);

}

Анализ программы.

-----------------------------------------------------------------

Первые три строки программы - описание функций; они вводятся

для того, чтобы описать как тип функций, так и порядок следования

их параметров с целью контроля ошибок.

Следующая строка описывает константу с плавающей точкой, ко-

торой присваивается имя INFINITY (в соответствии с принятым в Си

соглашением имена констант состоят из заглавных букв). Эта конс-

танта имеет очень большое положительное значение - близкое к наи-

большему из допустимых для типа float, и используется как флаг

возникновения ситуации деления на ноль (divide-by-zero).

Заметьте, что несмотря на то, что константа описана здесь,

она доступна внутри всех функций (включая главную функцию).

Далее следует функция main (главная функция), которая явля-

ется основным телом вашей программы. Каждая программа на Си обя-

зательно содержит функцию с именем main. Когда ваша программа на-

чинает выполняться, вызывается функция main и дальнейшее

выполнение программы продолжается под ее управлением. Когда вы-

полнение функции с именем main заканчивается, то завершается и

вся программа, после чего управление передается интегрированной

среде Турбо Си или, если вы выполняли программу непосредственно в

DOS, то монитору DOS.

Функция main может быть расположена в любом месте программы,

наиболее часто - это первая функция программы. Это обусловлено

- 465,466 -

тем, что расположение функции main в начале программы облегчает

ее чтение, описание прототипов функций и различных глобальных пе-

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

ментирование функций во всей программе.

После main следует фактическое определение трех функций,

прототипы которых были описаны выше: get_parms, get_ratio и

put_ratio. Рассмотрим отдельно каждое из определений.

Функция get_parms.

-----------------

Функция get_parms не возвращает никакого значения, так как

ее тип мы определили как void. Это было сделано в связи с тем,

что функция служит лишь для чтения двух значений и сохранения их

в некотором месте. В каком? Сейчас дадим некоторые пояснения.

Итак, мы передаем в get_parms два параметра. Эти параметры суть

адреса, по которым должны быть размещены считанные функцией зна-

чения. Обратите внимание! Оба параметра не являются данными типа

float, но являются указателями на переменные типа float. Другими

словами, мы работаем непосредственно с адресами памяти по которым

размещены переменные типа float.

В соответствии с этим и организовано обращение к функции из

main: когда мы вызываем get_parms из main, параметрами функции

являются &a и &b (адреса), а не текущие значения a и b. Заметьте

также, что вся информация, используемая при обращении к функции

scanf, находится непосредственно внутри функции get_parms и перед

параметрами p1 и p2 не стоят знаки операций адресации. Почему?

Потому, что p1 и p2 уже сами по себе адреса; они являются адреса-

ми переменных a и b.

Функция get_ratio.

-----------------

Функция get_ratio возвращает результат (типа float) обработ-

ки двух значений типа float, передаваемых ей (divident и

divisor). Возвращенный функцией результат зависит от того, равно

значение переменной divisor нулю или нет. Если значение divisor

равно нулю, get_ratio возвращает константу INFINITY. Если нет -

- 467,468 -

действительное значение частного двух чисел. Обратите внимание на

вид оператора return.

Функция put_ratio.

-----------------

Функция put_ratio не возвращает значение, т.к. ее тип объяв-

лен как void. Она имеет ровно один параметр - ratio, который ис-

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

следует выводить на экран дисплея. Если ratio равно INFINITY,

значит значение частного двух чисел считается неопределенным, в

противном случае значение ratio выводится на экран дисплея как

результат работы программы.

Глобальные описания.

-----------------------------------------------------------------

Константы, типы данных и переменные, описанные вне функций

(включая main), считаются глобальными ниже точки описания. Это

позволяет использовать их внутри функций в пределах всей програм-

мы вследствие того, что они уже описаны и известны во всех функ-

циях ниже точки описания. Если вы переместите описание INFINITY в

конец программы, то компилятор выдаст сообщение о том, что им об-

наружены две ошибки: одна из них - в get_ratio, a другая - в

put_ratio. Причина ошибок - использование неописанного идентифи-

катора.

Описание функций

-----------------------------------------------------------------

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

как классический стиль, так и современный. Классический стиль,

который нашел широкое применение в большинстве программ на Си,

имеет следующий формат:

- 469,470 -

тип имя_функции() ;

Эта спецификация описывает имя функции ("имя_функции") и тип

возвращаемых ею значений ("тип"). Это описание не содержит ника-

кой информации о параметрах функции, однако это не вызовет обна-

ружения ошибки компилятором или преобразования типов к типу, уже

принятому контекстно, в соответствии с принятыми соглашениями о

преобразовании типов. Если вы перепишите описания функций в

RATIO.C, используя этот стиль, то вновь полученные описания при-

обретут вид:

void get_parms();

float get_ratio();

void put_ratio();

Современный стиль используется в конструкциях расширенной

версии Си, предложенной ANSI. При описании функций в этой версии

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

нием "прототип функции". Описание функции с использованием ее

прототипа содержит дополнительно информацию о ее параметрах:

тип имя_функции(пар_инф1,пар_инф2,...)

где параметр пар_инф1 имеет один из следующих форматов:

тип

тип имя_пар

...

Другими словами, для использования прототипа функции должен

быть описан тип каждого формального параметра, либо указано его

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

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

использовать эллипсис (...).

Подход к описанию функций с помощью описания ее прототипа

дает возможность компилятору производить проверку на соответствие

количества и типа параметров при каждом обращении к функции. Это

также позволяет компилятору выполнять по возможности необходимые

преобразования. Обратите внимание, что описание функций в началь-

ной версии RATIO.C осуществлено с помощью прототипов функций. Бо-

лее полную информацию о прототипах функций можно получить в гла-

вах 11 и 12.

- 471,472 -

Определение функций

-----------------------------------------------------------------

Так же, как и в описании функций, при определении функций

прослеживается два стиля - классический и современный.

Классический формат определения функций имеет примерно сле-

дующий вид:

тип имя_функции(имена_параметров)

описание параметров;

{

локальные описания;

операторы;

}

Формат описания в современном стиле предусматривает опреде-

ление параметров функции в скобках, следующих за "имя_функции":

тип имя_функции(пар_инф, пар_инф, ...)

Однако, в этом примере определение параметра "пар_инф" со-

держит всю информацию о передаваемом параметре: тип и идентифика-

тор. Это позволяет рассматривать первую строку определения функ-

ции как часть соответствующего прототипа функции за одним важным

исключением: эта строка определения функции не содержит точку с

запятой (;) в определении, тогда как прототип функции всегда за-

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

get_parms в классическом стиле выглядит следующим образом:

void get_parms(p1, p2)

float *p1; float *p2;

{ ... }

Но приверженец современного стиля программирования на Си

опишет эту же функцию иначе, с использованием прототипа:

void get_parms(float *p1, float *p2)

{ ... }

Заметьте, что ряд описаний (константы, типы данных, перемен-

ные), содержащихся внутри функции (исключение составляет главная

функция main), "видимы" или определены только внутри этой функ-

ции. Поэтому Си не поддерживает вложенность функций; т.е. вы не

- 473,474 -

можете объявить одну функцию внутри другой.

Функции могут быть размещены в программе в различном порядке

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

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

пользовать функции, которые еще вами не определены и не описаны;

когда компилятор обнаружит функцию, которую прежде он не встре-

чал, он определит тип значений, возвращаемый функцией как int.

Если вы ранее определили тип возвращаемых ею значений как, напри-

мер, char*, то компилятор выдаст ошибку несоответствия типов дан-

ных.

Комментарии

-----------------------------------------------------------------

Итак, вы хотите внести в программу некоторые пояснения, ко-

торые напомнили бы вам (или информировали кого-нибудь другого),

что означает та или иная переменная, что дает та или иная функция

или оператор и т.д. Эти пояснения носят название "комментарий"

(comments). Си так же, как и большинство других языков программи-

рования, позволяет вносить комментарии в программы.

Начало комментария обозначается комбинацией знаков слеш и

звездочка (/*). После того, как компилятор обнаружит эту комбина-

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

тельности знаков */.

Комментарий может занимать несколько строк программы:

/* Это очень длинный комментарий,

занимающий более одной строки программы */

Проанализируйте еще раз расширенную версию RATIO.C и добавь-

те в нее комментарии.

- 475,476 -

Резюме

-----------------------------------------------------------------

Мы изучили семь основных конструкций языка программирования

и показали, как можно использовать каждую из них в рамках Турбо

Си.

На этом изучение основных конструкций Си не заканчивается,

мы вернемся к ним в главе 7, где рассмотрим их более подробно.

Г Л А В А 7

О НЕКОТОРЫХ ОСОБЕННОСТЯХ ПРОГРАММИРОВАНИЯ НА СИ

-----------------------------------------------------------------

Приятно знать, что вы добрались и до этой главы. В прошлой

главе мы дали вам представление о программировании на Турбо Си,

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

вы готовы углубиться в некоторые тонкости и тайны программирова-

ния на Си, в чем мы и постараемся вам помочь.

В этой главе...

-----------------------------------------------------------------

В этой главе мы предлагаем вам изучить следующее:

- структуры данных, включающие указатели, массивы и структуры;

- оператор switch;

- команды передачи управления, включающие return, break,

- 477,478 -

continue, goto и условный оператор (?:);

- потоки и поток ввода-вывода: как считывать из и записывать

на дисковый файл или встроенное оборудование;

- стиль программирования на Си и обзор новых расширений Си;

- некоторые общие "ловушки" для программистов на Си.

Обзор структур данных

-----------------------------------------------------------------

Основные типы данных мы рассмотрели в прошлой главе. К ним

относятся числа - целые и с плавающей точкой, символы и их разно-

видности. Теперь мы поговорим о том, как использовать эти элемен-

ты для построения СТРУКТУР данных. Но сначала мы исследуем одно

из важнейших понятий Си - указатели.

Указатели

-----------------------------------------------------------------

Большинство переменных, рассмотренных вами, в основном со-

держали данные, т.е. текущую информацию для работы вашей програм-

мы. Но иногда важнее знать место расположения данных, чем собс-

твенно их значение. Именно для этого и используются указатели.

Если вы имеете слабое представление о понятиях "адрес" и

"память", то вам просто необходимо ознакомиться с их кратким опи-

санием, которое мы приводим ниже.

- 479,480 -

Итак, ваш компьютер содержит в своей памяти (часто называе-

мой RAM - Random Access Memory - память произвольного доступа)

вашу программу и совокупность данных. На самом нижнем уровне па-

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

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

двух значений, обычно интерпретируемое как "0" и "1".

Восемь битов группируются в один БАЙТ. Большим группам битов

как правило, присваивается имя: обычно два байта составляют СЛО-

ВО, четыре байта составляют ДЛИННОЕ СЛОВО; и для IBM PC шестнад-

цать байт составляют ПАРАГРАФ.

Каждый байт в памяти вашего компьютера имеет собственный

уникальный адрес, так же, как каждый дом на любой улице. Но в от-

личие от большинства домов, последовательные байты имеют последо-

вательные адреса: если данный байт имеет адрес N, то предыдущий

байт имеет адрес N-1, а следующий - N+1.

УКАЗАТЕЛЬ - это переменная, содержащая адрес некоторых дан-

ных, а не их значение. Зачем это нужно?

Во-первых, мы можем использовать указатель места расположе-

ния различных данных и различных структур данных. Изменением ад-

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

вать, считывать, изменять) информацию в различных ячейках. Это

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

ных с помощью одного указателя.

Во-вторых, использование указателей позволит вам создавать

новые переменные в процессе выполнения программы. Си позволяет

вашей программе запрашивать некоторое количество памяти (в бай-

тах), возвращая адреса, которые можно запомнить в указателе. Этот

прием известен как ДИНАМИЧЕСКОЕ РАСПРЕДЕЛЕНИЕ; используя его, ва-

ша программа может приспосабливаться к любому объему памяти, в

зависимости от того как много (или мало) памяти доступно вашему

компьютеру.

В-третьих, вы можете использовать указатели для доступа к

различным элементам структур данных, таким как массивы, строки

или структуры. Указатель, в сущности, указывает место в памяти

вашего компьютера (а используя смещение относительно начального

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

те или иные данные. Индексируя указатель, вы получаете доступ к

- 481,482 -

некоторой последовательноси байтов, которая может представлять,

например, массив или структуру.

Теперь вы, несомнено, убеждены в удобстве указателей. А как

же их использовать в Си? Для начала вы должны их объявить.

Рассмотрим следующую программу:

main()

{

int ivar,*iptr;

iptr = &ivar;

ivar = 421;

printf("Размещение ivar: %p\n",&ivar);

printf("Содержимое ivar: %d\n", ivar);

printf("Содержимое iptr: %p\n", iptr);

printf("Адресуемое значение: %d\n",*iptr);

}

В ней объявлены две переменные: ivar и iptr. Первая, ivar -

это целая переменная, т.е. содержащая значение типа int. Вторая,

iptr - это указатель на целую переменную, следовательно она со-

держит АДРЕС значения типа int. Можно также сказать, что перемен-

ная iptr - это указатель, так как перед ее описанием стоит звез-

дочка (*). В языке Си эта звездочка называется косвенным

оператором.

В основном, данная программа делает следующее:

- адрес переменной ivar присваисвается iptr

- целое значение 421 присваивается ivar

Адресный оператор (&), как это было показано в предыдущей

главе, позволяет получить адрес, по которому размещено значение

переменной ivar.

Введя эту программу в свой компьютер и выполнив ее, вы

получите следующий результат:

Размещение ivar: 166E

Содержимое ivar: 421

Содержимое iptr: 166E

- 483,484 -

Адресуемое значение: 421

Первые две строки указывают адрес и содержимое ivar. Третья

представляет адрес, содержащийся в iptr. Как видите, это адрес

переменной ivar, т.е. место в памяти, где ваша программа решила

создать переменную с идентификатором ivar. В последней строке пе-

чатается то, что хранится по этому адресу - те же самые данные,

которые уже присвоены переменной ivar.

Заметим, что в третьем обращении к функции printf используе-

тся выражение iptr, содержимое которого есть адрес ivar. В пос-

леднем обращении к printf используется выражение *iptr, которое

позволяет получить данные, хранящиеся по этому адресу.

Рассмотрим теперь небольшую вариацию предыдущей программы:

main()

{

int ivar,*iptr;

iptr = &ivar;

*iptr = 421;

printf("Размещение ivar: %p\n",&ivar);

printf("Содержимое ivar: %d\n", ivar);

printf("Содержимое iptr: %p\n", iptr);

printf("Адресуемое значение: %d\n",*iptr);

}

В этой программе также адрес переменной ivar присваивается

iptr, но вместо присваивания числа 421 переменной ivar, это зна-

чение присваивается по указателю *iptr. Каков результат ? Точно

такой же, как и в предыдущей программе. Почему ? Потому что выра-

жения *iptr и ivar суть одна и та же ячейка памяти - поэтому в

этом случае оба оператора заносят значение 421 в одну и ту же

ячейку памяти.

- 485,486 -

Динамическое распределение

-----------------------------------------------------------------

Изменим еще раз нашу программу:

#include <alloc.h>

main()

{

int *iptr;

iptr = (int *) malloc(sizeof(int));

*iptr = 421;

printf("Содержимое iptr: %p\n", iptr);

printf("Адресуемое значение: %d\n",*iptr);

}

Эта версия позволяет вполне обойтись без описания переменной

ivar, которое непременно фигурировало в предыдущих примерах.

Вместо адреса этой переменной iptr присваивается значение (тоже

адрес некоторой ячейки памяти), возвращаемое некоторой функцией,

которая называется malloc, и описана в библиотеке alloc.h (отсюда

появление директивы #include в начале программы). Затем по этому

адресу присваивается значение 421 и переменная *iptr вновь, как и

в предыдущем примере, принимает значение 421.

Обратите внимание, что если вы теперь выполните программу,

то получите иное значение iptr, чем раньше, но значение *iptr ос-

танется равным 421.

Разберем теперь, что же делает оператор

iptr = (int *) malloc(sizeof(int));

Разобьем его на части:

- выражение sizeof(int) возвращает количество байтов,

требуемое для хранения переменной типа int; для компи-

лятора Турбо Си, работающего на IBM PC, это возвращае-

мое значение равно 2.

- функция malloc(num) резервирует num последовательных

байтов доступной (свободной) памяти в компьютере, а за-

тем возвращает начальный адрес размещения в памяти этой

последовательности байтов.

- 487,488 -

- выражение (int *) указывает, что этот начальный адрес

суть указатель на данные типа int. Выражение такого ви-

да известно как выражение приведения типа (type

casting). В данном случае Турбо Си не требует обяза-

тельного его применения. Но в связи с тем, что для дру-

гих компиляторов Си это выражение является обязатель-

ным, при его отсутствии вы получите сообщение об

ошибке:

" Non-portable pointer assignment."

(Непереносимое в другие системы присваивание указателя)

Из соображений переносимости программного обеспечения,

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

своих программах.

- наконец, адрес, полученный с помощью функции malloc,

запоминается в iptr. Таким образом, вами получена дина-

мически созданная целая переменная к которой вы можете

обращаться при помощи идентификатора *iptr.

Весь этот блок можно описать следующим образом: "выделить в

памяти компьютера некоторый участок для переменной типa int, за-

тем присвоить начальный адрес этого участка переменной iptr, яв-

ляющейся указателем на переменную типа int".

Необходимо ли все это? Да. Почему? Потому что без этого у

вас нет гарантии, что iptr указывает на свободный участок памяти.

iptr будет содержать некоторое значение, которое вы будете ис-

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

пользуется ли уже этот раздел памяти для других целей. Правило

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

адрес до своего использования в программе.

Иными словами, не присваивайте целое значение *iptr без

предварительного присвоения адреса в iptr.

- 489,490 -

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

-----------------------------------------------------------------

В прошлой главе мы объяснили, как объявлять параметры функ-

ций. Возможно теперь вам более понятна причина использования ука-

зателей в качестве формальных параметров функции, значения кото-

рых вы можете изменять.

Рассмотрим, например, следующую функцию:

void swap(int *a, int *b)

{

int temp;

temp = *a; *a = *b; *b =temp;

}

Эта функция swap (обмен) объявляет два формальных параметра

a и b, как указатели на некие данные типа int. Это означает, что

функция swap работает с адресами целых переменных (а не с их зна-

чениями). Поэтому будут обработаны данные, адреса которых переда-

ны функции во время обращения к ней.

Далее представлена программа, вызывающая swap:

main()

{

int i,j;

i = 421;

j = 53;

printf("До обращения: i=%4d, j=%4d\n",i,j);

swap(&i,&j);

printf("После обращения: i =%4d, j=%4d\n",i,j);

}

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

i на значение j (переставляет их местами). Заменим эту программу

на аналогичную ей, раскрыв процедуру swap в теле программы:

main()

{

int i,j;

- 491,492 -

int *a,*b,temp;

i = 421;

j = 53;

printf("До обработки: i = %4d j = %4d\n",i,j);

a = &i;

b = &j;

temp = *a; *a = *b; *b =temp;

printf("После обработки: i = %4d j = %4d\n",i,j);

}

Эта программа, конечно, приводит к тому же результату, что и

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

swap(&i,&j) значения двух фактических параметров (&i,&j) присваи-

ваются двум формальным параметрам (a и b), обрабатываемым непос-

редственно операторами функции swap.

Адресная арифметика

-----------------------------------------------------------------

Каким образом вам необходимо поступить, если вы хотите так

модифицировать программу, чтобы iptr указывала на три переменных

типа int вместо одной? Далее представлено одно из возможных реше-

ний:

#include <alloc.h>

main()

{

#define NUMINTS 3

int *list,i;

list = (int *) calloc(NUMINTS,sizeof(int));

*list = 421;

*(list+1) = 53;

*(list+2) = 1806;

printf("Список адресов :");

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

printf("%4p ",(list+i));

printf("\nСписок значений :");

- 493,494 -

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

printf("%4p ",*(list+i));

printf("\n");

}

Вместо функции malloc эта программа использует функцию

calloc c двумя параметрами: первый показывает, для скольких эле-

ментов будет происходить резервирование памяти, второй - величину

каждого элемента в байтах. После обращения к функции calloc, list

указывает на участок памяти размером 6 (3*2) байта, достаточный

для хранения трех переменных типа int.

Более подробно рассмотрим следующие три оператора. Первый

оператор вам знаком - *list = 421. Он означает: "запомнить 421 в

переменной типа int, расположенной по адресу , хранящемуся в пе-

ременной list".

Следующий: *(list+1) = 53 - особенно важен для понимания. На

первый взгляд, его можно интерпретировать так: "запомнить 53 в

переменной типа int, расположенной байтом дальше адреса, храняще-

гося в list". Если это так, то вы окажетесь в середине предыдущей

int-переменной (которая имеет длину 2 байта). Это, естественно,

испортит ее значение.

Однако, ваш компилятор с Си не сделает такой ошибки. Он "по-

нимает", что list - это указатель на тип int, и поэтому выражение

list+1 представляет собой адрес байта, определенного выражением

list+(1*sizeof(int)), и, поэтому, значение 53 не испортит значе-

ния 421 (т.е. для этого значения будет выделена другая ячейка па-

мяти).

Аналогично, (list+2)=1806 представляет адрес байта

list+(2*sizeof(int)) и 1806 запоминается, не затрагивая два пре-

дыдущих значения.

В общем, ptr+i представляет адрес памяти, определяемый

выражением ptr+(i*sizeof(int)).

Введите и выполните вышеописанную программу; на выходе вы

получите следующее:

Список адресов : 066A 06AC 06AE

Список значений: 421 53 1806

- 495,496 -

Заметьте, что адреса различаются не в один байт, а в два, и

все три значения хранятся отдельно.

Подведем итог: Если вы используете ptr, указатель на тип

type, то выражение (ptr+1) представляет адрес памяти

(ptr+(1*sizeof(type)), где sizeof(type) возвращает количество

байт, занимаемых переменной типа type.

Массивы

-----------------------------------------------------------------

Большинство языков высокого уровня - включая Си - позволяют

определять МАССИВЫ, т.е. индексированный набор данных определен-

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

программу следующим образом:

main()

{

#define NUMINTS 3

int list[NUMINTS],i;

list[0] = 421;

list[1] = 53;

list[2] = 1806;

printf("Список адресов:");

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

printf("%p ",&list[i]);

printf("\nСписок значений:");

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

printf("%4p ",list[i]);

- 497,498 -

printf("\n");

}

Выражение int list[NUMINTS] объявляет list как массив пере-

менных типа int, c объемом памяти, выделяемым для трех целых пе-

ременых.

К первой переменной массива можно обращаться как к list[0],

второй - как к list[1] и третьей - как к list[2].

В общем случае описание любого массива имеет следующий вид:

type name[size]

(тип имя [размер])

где type - тип данных элементов массива (любой из допустимых в

языке),

name - имя массива.

Первый элемент массива - это name[0], последний элемент -

name[size-1]; общий объем памяти в байтах оперделяется выражением

size*(sizeof(type)).

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

-----------------------------------------------------------------

Вы, наверное, уже поняли , что существует определенная связь

между массивами и указателями. Поэтому, если вы выполните только

что рассмотренную программу, полученный результат будет вам уже

знаком:

список адресов : 163A 163C 163E

список значений: 421 53 1806

Начальный адрес другой, но это единственное различие. В са-

мом деле, имя массива можно использовать как указатель; более то-

го, вы можете определить указатель как массив. Рассмотрим следую-

щие важные тождества:

(list + i) == &(list[i])

*(list + i) == list[i]

В обоих случаях выражение слева эквивалентно выражению спра-

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

внимание, описан ли list как указатель, или как массив.

- 499,500 -

Единственное различие между описанием list,как указателя или

как массива состоит в размещении самого массива. Если вы описали

list как массив, то программа автоматически выделяет требуемый

объем памяти. Если же вы описали переменную list как указатель,

то вы сами обязательно должны выделить память под массив, исполь-

зуя для этого функцию calloc или сходную с ней функцию, или же

присвоить этой переменной адрес некоторого сегмента памяти, кото-

рый уже был определен ранее.

Массивы и строки

-----------------------------------------------------------------

Мы говорили о строках в предыдущей главе и обращались при

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

строку как указатель на символы и как массив символов. Теперь вы

можете лучше понять это различие.

Если вы описываете строку как массив типа char, то память

для этой строки резервируется автоматически. Если же вы описывае-

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

руется: вы должны либо сами выделить ее (используя функцию malloc

или ей подобную), или же присвоить ей адрес существующей строки.

Пример этой ситуации дается далее в разделе "Ловушки программиро-

вания на Си" этой главы.

Многомерные массивы

-----------------------------------------------------------------

Да, вы действительно можете использовать многомерные масси-

- 501,502 -

вы, и они описываются именно так, как вы себе и представляли:

type name[size1][size2]...[sizeN]

(тип имя [размер1][размер2]...[размерN])

Рассморим следующую программу, которая определяет два дву-

мерных массива, а затем выполняет их матричное умножение:

main()

{

int a[3][4] = {{ 5, 3, -21, 42},

{44, 15, 0, 6},

{97 , 6, 81, 2}};

int b[4][2] = {{22, 7},

{97, -53},

{45, 0},

{72, 1} };

int c[3][2],i,j,k;

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

for (j=0; j<2; j++) {

c[i][j] = 0;

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

c[i][j] += a[i][k] * b[k][j];

}

}

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

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

printf("c[%d][%d] = %d ",i,j,c[i][j]);

printf("\n");

}

}

Отметим два момента в этой программе. Синтаксис определения

двумерного массива состоит из набора {...} списков, разделенных

запятой. Квадратные скобки ([ ]) используются для записи каждого

индекса.

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

сис [i,j]. Так можно написать и на Си, но это все равно, что ска-

зать просто [j], т.к. запятая интерпретируется как оператор, оз-

начающий (" определить i, затем определить j, затем присвоить

всему выражению значение j").

- 503,504 -

Для полной уверенности ставьте квадратные скобки вокруг

каждого индекса.

Многомерные массивы хранятся в памяти слева направо по пра-

вилу "строки - столбцы". Это означает, что последний индекс изме-

няется быстрее. Другими словами, в массиве arr[3][2] элементы arr

хранятся в памяти в следующем порядке:

arr[0][0]

arr[0][1]

arr[1][0]

arr[1][1]

arr[2][0]

arr[2][1]

Тот же принцип сохраняется для массивов двух-, трех- и

большей размерности.

Массивы и функции

-----------------------------------------------------------------

Что произойдет, если вы захотите передать массив в функцию?

Рассмотрим следующую функцию, возвращающую индекс минимального

числа массива int:

int imin(int list[], int size)

{

int i, minindx, min;

minindx = 0;

min = list[minindx];

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

if (list[i] < min) {

min = list[i];

minindx = i;

}

return(minindx);

}

Здесь вы видите одну из важных особенностей Си: вам необяза-

тельно знать при трансляции величину list[]. Почему? Потому что

компилятор считает list[] начальным адресом массива, и не забо-

- 505,506 -

тится о том, где его конец.

Программа, обращающаяся к функции imin, может выглядеть так:

main()

{

#define VSIZE 22

int i,vector[VSIZE];

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

vector[i] = rand();

printf("vector[%2d] = %6d\n",i,vector[i]);

}

i = imin(vector,VSIZE);

printf("minimum: vector[%2d] = %6d\n",i,vector[i]);

}

Может возникнуть вопрос: что именно передается в imin? В

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

чает, что если вы производите какие-либо изменения массива list в

imin то, те же изменения будут произведены и в массиве vector.

Например, вы можете написать следующую функцию :

void setrand(int list[],int size);

{

int i;

for (i = 0; i < size; i++) list[i] = rand();

}

Теперь для инициализации массива vector вы можете написать в

main setrand(vector,VSIZE). Следует заметить, что массиву vector

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

работы датчика случайных чисел, эмулируемого функцией Турбо Си

rand().

А как передавать многомерный массив? Имеется ли такая воз-

можность? Предположим, вы хотите модифицировать setrand для рабо-

ты с двумерным массивом. Вы должны написать приблизительно следу-

ющее:

void setrand(int matrix[][CSIZE],int rsize)

{

int i,j;

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

- 507,508 -

for (j = 0; j < CSIZE; i++)

matrix[i][j] = rand();

}

}

Где CSIZE - это глобальная константа, определяющая второе

измерение массива. Другими словами, любой массив, передаваемый

setrand получит второе измерение массива, равный CSIZE.

Однако, есть еще одно решение. Предположим, у вас есть мас-

сив matrix[15,7], который вы хотите передать в setrand. Если вы

используете следующее описание:

setrand(int list[],int size)

обращение к функции будет иметь вид:

setrand(matrix,15*7);

Массив matrix будет рассматриваться функцией setrand как од-

номерный массив, содержащий 105 элементов (15 строк * 7 столб-

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

Структуры

-----------------------------------------------------------------

Массивы и указатели позволяют вам создавать список элементов

одного типа. А что, если вы хотите создать нечто, содержащее эле-

менты различного типа? Для этого используются СТРУКТУРЫ.

Структура - это конгломерат элементов различного типа. До-

пустим, вы хотите сохранить информацию о звезде: ее имя, спект-

ральный класс, координаты и прочее. Вы можете описать это следую-

щим образом:

typedef struct {

char name[25];

char class;

short subclass;

float decl,RA,dist;

} star ;

Здесь определена структура (struct) типа star. Сделав такое

описание в начале своей програмы, вы можете дальше использовать

этот определенный вами тип данных:

- 509,510 -

main()

{

star mystar;

strcpy(mystar.name,"Эпсилон Лебедя");

mystar.class = 'N';

mystar.subclass = 2;

mystar.decl = 3.5167;

mystar.RA = -9.633;

mystar.dist = 0.303;

/* конец функции main() */

}

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

составное имя, состоящее из имени структуры (на первом месте) и,

в порядке иерархической подчиненности, имен ее образующих элемен-

тов, разделенных точками (.). Конструкция вида: varname.memname

(имя переменной.имя элемента) считается эквивалентной имени пере-

менной того же типа, что и memname, и вы можете выполнять с ней

те же операции.

- 511,512 -

Структуры и указатели

-----------------------------------------------------------------

Вы можете описывать указатели на структуры точно так же, как

и указатели на другие типы данных. Это необходимо для создания

связных списков и других динамических структур данных элементами

которых, в свою очередь, являются структуры данных.

Фактически указатели на структуры так часто используются в

Си, что существует специальный символ для ссылки на элемент

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

предыдущей программы:

#include <alloc.h>

main()

{

star *mystar;

mystar = (star *) malloc(sizeof(star));

strcpy(mystar -> name,"Эпсилон Лебедя");

mystar -> class = 'N';

mystar -> subclass = 2;

mystar -> decl = 3.5167;

mystar -> RA = -9.633;

mystar -> dist = 0.303;

/* Конец функции main() */

}

В этом варианте mystar объявляется как указатель типа star,

а не как переменная типа star. Память для mystar резервируется

путем обращения к функции malloc. ТЕПЕРЬ, КОГДА ВЫ ССЫЛАЕТЕСЬ НА

ЭЛЕМЕНТЫ mystar, ИСПОЛЬЗУЙТЕ ptrname -> memnane. Символ -> озна-

чает, что "элемент структуры направлен в ..."; это сокращенный

вариант от обозначения (*ptrname).memnane принятый в Си.

- 513,514 -

Оператор switch (переключатель)

-----------------------------------------------------------------

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

if..else if..else и т.д. Рассмотрим следующую функцию:

#include <ctype.h>

do_main_menu(short *done)

{

char cmd;

*done = 0;

do {

cmd = toupper(getch());

if (cmd == 'F') do_file_menu(done);

else if (cmd == 'R') run_program();

else if (cmd == 'C') do_compile();

else if (cmd == 'M') do_make();

else if (cmd == '?') do_project_menu();

else if (cmd == 'O') do_option_menu();

else if (cmd == 'E') do_error_menu();

else handle_others(cmd,done);

} while (!*done);

}

Подобная ситуация встречается настолько часто, что в Си была

была введена специальная управляющая структура которая носит наз-

вание оператор switch. Вот та же функция, но записанная с исполь-

зованием оператора switch :

#include <ctype.h>

do_main_menu(short *done)

{

char cmd;

*done = 0;

do {

cmd = toupper(getch());

switch(cmd) {

case 'F' : do_file_menu(done); break;

- 515,516 -

case 'R' : run_program(); break;

case 'C' : do_compile(); break;

case 'M' : do_make(); break;

case '?' : do_project_menu(); break;

case 'O' : do_option_menu(); break;

case 'E' : do_error_menu(); break;

default : handle_others(cmd,done);

} while (!*done);

}

Эта функция организует цикл, в котором символ считывается,

преобразуется к значению на верхнем регистре, а затем запоминает-

ся в переменной cmd. Потом введенный символ обрабатывается

оператором switch на основе значения cmd.

Цикл повторяется до тех пор, пока переменная done не станет

равной нулю (предположительно в функции do_file_menu или

handle_others).

Оператор switch получает значение cmd и сравнивает его с

каждым значением метки case. Если они совпадают, начинается вы-

полнение операторов данной метки, которое продолжается либо до

ближайшего оператора break, либо до конца оператора switch. Если

ни одна из меток не совпадает, и вы включили метку default в опе-

ратор switch, то будут выполняться операторы этой метки; если

метки default нет, оператор switch целиком игнорируется.

Значение value, используемое в switch(value) должно быть

приведено к целому значению. Другими словами, это значение должно

легко преобразовываться в целое для таких типов данных как char,

разновидности enum и, конечно, int, а также всех его вариантов.

Нельзя использовать в операторе switch вещественные типы данных

(такие как float и double), указатели, строки и другие структуры

данных, но разрешается использовать элементы структур данных сов-

местимых с целыми значениями.

Хотя (value) может быть выражением (константа, переменная,

вызов функции, и другие комбинации их), метки case должны содер-

жать константы. Кроме того, в качестве ключевого значения case

может быть только одно значение. Если бы do_main_menu не исполь-

зовало фукцию toupper для преобразования cmd, оператор switch мог

бы выглядеть следующим образом:

switch (cmd) {

- 517,518 -

case 'f' :

case 'F' : do_file_menu(done);

break;

case 'r' :

case 'R' : run_program();

break;

...

Этот оператор выполняет функцию do_file_menu независимо от

того, в каком регистре поступает значение cmd, аналогично выпол-

няются действия для других альтернатив значения cmd.

Запомните, что для завершения данного case вы должны исполь-

зовать оператор break . В противном случае будут выполняться пос-

ледовательно все операторы, относящиеся к другим меткам (до тех

пор пока не встретится оператор break).

Если вы уберете оператор break после вызова do_file_menu, то

при вводе символа F будет вызываться do_file_menu, а затем будет

вызвана функция run_program.

Однако иногда вам нужно сделать именно так. Рассмотрим

следующий пример:

typedef enum( sun, mon, tues, wed, thur, fri, sat, ) days;

main()

{

days today;

...

swith (today) {

case mon :

case tues :

case wed :

case thur :

case fri : puts("Иди работать!");break;

case sat : printf("Убери во дворе и ");

case sun : puts("Расслабься!");

}

В этом операторе switch для значений от mon до fri выполня-

ется одна и та же функции put, после которой оператор break ука-

- 519,520 -

зывает на выход из switch. Однако, если today равно sat, выполня-

ется соответствующая функция printf, а затем выполняется puts

("Расслабься!"); если же today равно sun, выполняется только пос-

ледняя функция puts.

Команды передачи управления

----------------------------------------------------------------

Это дополнительные команды, предназначенные для использова-

ния в управляющих операторах или для моделирования других управ-

ляющих структур. Оператор return позволяет вам досрочно выйти из

функции. Операторы break и continue предназначены для использова-

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

мы. Условный оператор (?:) позволят сжать определенные выражения

типа if..else в одну строку.

Один совет: подумайте дважды перед использованием каждой ко-

манды передачи управления (за исключением, конечно, return). Ис-

пользуйте их в тех случаях, когда они представляют наилучшее ре-

шение, но помните, что чаще всего вы можете решить возникшую

перед вами проблему проблему без их помощи. Особенно избегайте

оператора goto: операторы return, break или continue наверняка

заменят его вам.

- 521,522 -

Оператор return

-----------------------------------------------------------------

Существует два основных способа использования оператора

return.

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

и вам необходимо использовать его в зависимости от того, какое

значение возвращается в вызыващую программу, например:

int imax(int a, int b);

{

if (a > b) return(a);

else return(b);

}

Здесь функция использует оператор return для возвращения

максимального из двуx переданныx ей значений.

Второй способ использования оператора return состоит в воз-

можности выxода из функции в некоторой точке до ее завершения.

Например, функция может определить условие, по которому произво-

дится прерывание. Вместо того, чтобы помещать все основные опера-

торы функции внутрь оператора if, для выхода можно использовать

оператор return. Если функция имеет тип VOID (т.е. не возвращаю-

щает никакого значения), можно написать return без возвращаемого

значения.

Рассмотрим модификацию программы imin, предложенную ранее:

int imin(int list[], int size))

{

int i, minindx, min;

if (size <= 0) return(-1);

...

}

В этом примере, если параметр size меньше либо равен нулю,

то массив list пуст, в связи с чем оператор return вызывает выход

из функции.

Заметим, что в случае ошибки возвращается значение -1. Т.к.

-1 никогда не может быть индексом массива, вызывающая программа

- 523,524 -

регистрирует факт возникновения ошибки.

Оператор break

-----------------------------------------------------------------

Иногда бывает необходимо выйти из цикла до его завершения.

Рассмотрим следующую программу:

#define LIMIT 100

#define MAX 10

main()

{

int i,j,k,score;

int scores[LIMIT][MAX];

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

j = 0;

while (j < MAX-1) {

printf("Введите следующее значение #%d: ",j);

scanf("%d", score);

if (score < 0)

break;

scores[i][++j] = score;

- 525,526 -

}

scores[i][0] = j;

}

}

Рассмотрим оператор if (score < 0) break;. Он указывает, что

если пользователь введет отрицательное значение score, цикл while

прерывается. Переменная j используется и в качестве индекса

scores и в качестве счетчика общего количества элементов в каждой

строке; это значение записывается в первом элементе строки.

Вспомните, пожалуйста, использование оператора break в опе-

раторе switch, представленное ранее. Там break указывает програм-

ме выйти из оператора switch; здесь он указывает программе выйти

из цикла и продолжить работу. Кроме оператора switch оператор

break может быть использован во всех трех циклах (for, while и do

... while), однако его нельзя использовать в конструкции if ...

else или в теле главной процедуры main для выхода из нее.

Оператор continue

----------------------------------------------------------------

Иногда нужно не выходить из цикла, а пропустить ряд операто-

ров в теле цикла и начать его заново. В этом случае можно приме-

нить оператор continue, предназначенный специально для этого. Об-

ратите внимание на следующую программу:

#define LIMIT 100

#define MAX 10

main()

{

int i,j,k,score;

int scores[LIMIT][MAX];

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

j = 0;

while (j < MAX-1) (

printf("Введите следующее значение #%d: ",j);

scanf("%d", score);

if (score < 0)

continue;

- 527,528 -

scores[i][++j] = score;

)

scores[i][0] = j;

)

}

Когда выполняется оператор continue, программа пропускает

остаток цикла и начинает цикл сначала. В результате эта программа

работает иначе, чем предыдущая. При вводе пользователем числа -1

считатся что была сделана ошибка, и вместо выхода из внутреннего

цикла цикл while начинается сначала. Поскольку значение j не было

увеличено, программа снова просит ввести то же значение.

Оператор goto

-----------------------------------------------------------------

Да, в Си действительно есть оператор goto. Формат простой:

goto метка, где "метка" - любой идентификатор, связанный с опре-

деленным выражением. Однако наиболее разумное решение при прог-

раммировании на Си - обойтись без использования оператора goto.

Для этого предусмотрено три оператора цикла. Подумайте вниматель-

но, прежде чем использовать оператор goto, действительно ли он

вам нужен в создавшейся ситуации и может быть его можно заменить

на оператор цикла?.

Условный оператор (?:)

-----------------------------------------------------------------

В некоторых случаях необходимо произвести выбор между двумя

альтернативами (и результирующими значениями), основанный на не-

котором условии. Обычно это реализуется оператором if ... else,

например, так:

- 529,530 -

int imin(int a, int b)

(

if (a < b) return(a);

else return(b);

)

Но, как оказывается, для реализации такого типа выбора дос-

таточно одной специальной конструкции. Ее формат следующий:

выражение 1 ? выражение 2 : выражение 3.

А смысл таков: "если выражение 1 верно, то вычисляется выра-

жение 2 и все выражение получает его значение; иначе вычисляется

выражение 3 и передается его значение". Используя эту конструк-

цию, imin можно представить следующим образом:

int imin(int a, int b)

(

return((a < b) ? a : b );

)

более того,можно даже записать imin как строку макроса:

#define imin(a,b) ((a < b) ? a : b)

Теперь, где бы ваша программа ни встретила выражение

imin(e1,e2), она замещает его на ((e1<e2) ? e1 : e2) и продолжает

вычисления. Это в действительности наиболее общее решение, так

как a и b больше не ограничены типом int; они могут быть любого

типа, с которым можно выполнить операцию сравнения.

- 531,532 -

Потоки и поток ввода - вывода

-----------------------------------------------------------------

Что такое потоки?

-----------------------------------------------------------------

Потоки - это наиболее переносимые средства для чтения или

записи данных в Турбо Си. Они позволяют разрабатывать гибкий и

эффективный ввод-вывод, который не зависит от используемых файлов

или встроенного оборудования.

Поток является файлом или физическим устройством (принтером

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

телей на объект FILE (определенный в stdio.h). Объект FILE содер-

жит различную информацию о потоке, включая текущую позицию пото-

ка, указателм на соответствующие буферы и индикаторы ошибки или

конца файла.

Ваша программа никогда не может саздавать и копировать не-

посредственно объекты FILE; однако, она может использовать указа-

тели, возвращенные из функций типа fopen. Убедитесь, что нет про-

тиворечия между указателями FILE и файловым управлением DOS (при

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

вода).

Вы должны открыть поток перед тем, как вы будете производить

ввод-вывод в него. Открытый поток связывается с названным в DOS

файлом или устройством. Программы, которые открывают потоки, это

fopen, fdopen и freopen. Когда вы открываете поток, вы указывае-

те, хотите ли вы читать или записывать в поток, или делать и то и

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

из потока как с текстом или как с двоичными данными. Это послед-

нее указание важно для уменьшения несовместимости между потоками

ввода-вывода Си и текстовым файлом DOS.

Текстовые и двоичные потоки

-----------------------------------------------------------------

Текстовые потоки используются для обычных текстовых файлов

DOS, таких как файл, созданный редактором Турбо Си. Поток вво-

да-вывода Си предполагает, что текстовый файл разделен на строки,

отделенные с помощью одного символа новой строки (которым являет-

- 533,534 -

ся ASCII символ перевода строки). Текстовый файл DOS, однако, за-

писывается на диск с двумя символами между каждой строкой - ASCII

символами возврат каретки и перевод строки. В текстовом режиме

Турбо Си преобразует пару возврат каретки/перевод строки (CR/LF)

в один символ перевод строки при вводе; перевод строки преобразу-

ется в пару CR/LF при выводе.

Двоичный поток значительно проще, чем текстовый. Никаких

преобразований не производится. Любые символы читаются и записы-

ваются без изменений.

Файл может быть объявлен и в текстовом и в двоичном режиме

без всяких проблем, если вы осведомлены и понимаете преобразова-

ния, имеющие место в текстовом потоке. Турбо Си не "запоминает",

как файл был создан или модифицирован.

Если режим преобразования не указывается, когда поток откры-

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

ной переменной _fmode. По умолчанию _fmode настроена на текстовый

режим.

Буферизация потоков

-----------------------------------------------------------------

Потоки, связанные с файлами, обычно буферизируются. Это поз-

воляет очень быстро вводить или выводить одиночные символы так,

как это делают getc и putc. Вы можете создать ваш собственный бу-

фер, изменить размер используемого буфера, или заставить поток не

использовать буфер с помощью вызова setvbuf или setbuf.

Буфер автоматически очищается при его заполнении, при созда-

нии потока и при нормальном завершении программы. Вы можете ис-

пользовать fflush и flushall для очистки буфера.

Обычно, вы используете потоки для чтения и записи данных

последовательно. Ввод-вывод располагается в текущей позиции фай-

ла. Когда вы записываете или считываете данные, программа переме-

щает файловую позицию непосредственно за обработанными данными.

Поток, который связан с дисковым файлом, может быть также потоком

произвольного доступа. Вы можете использовать fseek для позицио-

нирования файла, а затем результатом выполнения операторов чтения

или записи будет доступ к данным после этой точки.

- 535,536 -

Если вы одновременно записываете и считываете данные из по-

тока, то вы не можете свободно смешивать операторы чтения и запи-

си. Вы должны очистить буфер потока между записью и чтением. Вы-

зов fflush, flushall или fseek очищает буфер и позволяет вам сме-

нить операции. Для максимальной переносимости вы должны очищать

буфер, даже если он не используется, так как другие системы могут

иметь дополнительные ограничения на смешивание операций ввода-вы-

вода даже без буфера.

Предопределенные потоки

-----------------------------------------------------------------

В дополнение к потокам, создоваемым вызовом fopen, пять пре-

допределенных потоков доступны всякий раз, когда ваша программа

начинает исполнятся.

-------------------------------------------------------

Имя ввод-вывод Режим Поток

-------------------------------------------------------

stdin ввод Текстовый Стандартный ввод

stdout вывод Текстовый Стандартный вывод

stderr вывод Текстовый Стандартная ошибка

stdaux ввод-вывод Двоичный Вспом. ввод-вывод

stdprn вывод Двоичный Принтерный вывод

-------------------------------------------------------

Потоки stdaux и stdprn специфичны для DOS и не переносимы на

другие системы.

Потоки stdin и stdout могут быть перенаправлены DOS, в то

время как другие связаны с особыми устройствами: stderr - с кон-

- 537,538 -

солью (CON:), stdprn - с принтером (PRN:), а stdaux - со вспомо-

гательным портом.

Вспомогательный порт зависит от конфигурации вашей машины;

обычно это COM1:. Посмотрите в документации вашей DOS о перенап-

равлении ввода и вывода в командной строке DOS. Без перенаправле-

ния потоки stdin и stdout связаны с консолью (CON:). Кроме того,

если нет перенаправления, stdin имеет строку буфера, а stdout не

буферизируется. Другие предопределенные потоки тоже не буферизи-

руются.

Для установки предопределенного потока в режим отличный от

режима по умолчанию (например, для установки stdprn в текстовый

режим) используйте setmode. Имя предопределенного потока является

константой и вы не можете присваивать ему значения. Если вы хоти-

те связать один из этих потоков с файлом или устройством, то ис-

пользуйте freopen.

Программирование на Си: классический и современный стили

----------------------------------------------------------------

В программировании на Си существует несколько общераспрост-

раненных тенденций, объединяющих известные технические приемы,

облегчающие использование Си. Многие из этих тенденций противоре-

чат классическим традициям и методам программирования на Си.

Большинство из них стали возможны благодаря расширению языка ANSI

C Standart Committee. Этот раздел ознакомит вас с приемами, ис-

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

писании более удобных программ на Си.

Турбо Си позволяет следовать наравне классическому и совре-

менному стилям.

- 539,540 -

Использование прототипов функций

и полных определений функций

-----------------------------------------------------------------

В классическом стиле программирования на Си вы определяете

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

Например, вы определяете функцию swap следующим образом:

int swap();

Никакой информации о параметрах не передается: ни о коли-

честве их, ни о типе. Классическое определение функции выглядит

так:

int swap(a,b)

int *a, *b;

{

/* Тело функции */

}

Результат такого определения - отсутствие контроля над ошиб-

ками при использовании параметров, что приводит к появлению хит-

рых и труднообнаружимых ошибок.

Избегайте такого стиля.

Современный стиль включает использование прототипов функций

(для описания функций) и списка параметров (для определения функ-

ций). Давайте переопределим swap, используя прототипы:

int swap(int *a, int *b);

Теперь при выполнении ваша программа получает всю необходи-

мую информацию, требуемую для выполнения контроля над ошибками

при любом обращении к swap. Теперь вы можете использовать простой

формат и при определении функции:

int swap(int *a, int *b)

{

/* тело функции */

}

Современный стиль программирования повышает вероятность об-

- 541,542 -

наружения ошибки, даже если вы не используете прототипов функций;

если же вы используете прототипы, то компилятор автоматически от-

слеживает и обеспечивает соответствие описаний и определений.

Использование ключевого слова enum

-----------------------------------------------------------------

В классическом языке Си списки определяются директивой

#define следующим образом:

#define sun 0

#define mon 1

#define tues 2

#define wed 3

#define thur 4

#define fri 5

#define sat 6

Однако в нашей версии Си введен так называемый перечислимый

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

enum, как это показано далее:

typedef enum(sun,mon,tues,wed,thur,fri,sat) days;

Результат здесь тот же, что и в классическом методе: присво-

ение осуществляется в порядке возрастания значений от sun=0 до

sat = 6. Однако современный метод позволяет представлять информа-

цию более абстрактно и содержательно, чем длинный список директив

#define. Кроме того, вы можете описать переменные принадлежащими

к абстрактному типу данных days.

Использование директивы typedef

-----------------------------------------------------------------

В классическом Си определенные пользователем типы данных

именуются редко, за исключением структур и объединений - перед

любым объявлением которых вы ставите ключевые слова struct или

union.

- 543,544 -

В современом Си обеспечивается другой уровень информационной

содержательности путем использования директивы typedef. Она поз-

воляет связать нужный тип данных (включая struct и enum) с неко-

торым именем, а затем объявить переменые этого типа. Далее предс-

тавлен пример определения типа и определение переменных этого

нового, введенного пользователем, типа данных:

typedef int *intptr;

typedef char namestr[30];

typedef enum ( male, female, unknown) sex;

typedef struct (

namestr last, first;

char ssn[9];

sex gender;

short age;

float gpa;

) student;

typedef student class[100];

class hist104,ps102;

student valedictorian;

intptr iptr;

Использование typedef делает программу более читабельной, а

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

для определения типов данных, а распространить их определение на

всю программу по мере их появления и использования в ней.

Описание функции void

----------------------------------------------------------------

В начальной версии языка Си каждая функция возвращала значе-

ние некоторого типа; если же тип не был описан, то по умолчанию

функции присваивался тип int. Подобно этому функция, возвращающая

"сгенерированные" (нетипичные) указатели, обычно описывалась как

возвращающая указатель типа char, только потому, что она должна

была хоть что-то возвращать.

В нашей же версии Си существует стандартный тип void, кото-

рый представляется как разновидность "нулевого" типа. Любая функ-

ция, не возвращающая явно какое-либо значение, может быть объяв-

лена как функция типа void. Заметим, что большинство программ,

- 545,546 -

использующих динамическое распределение памяти (например malloc),

описываются как имеющие тип void. Это означает, что они возвраща-

ют нетипизированный указатель, значение которого вы затем (в Тур-

бо Си) можете присвоить указателю любого типа данных без предва-

рительного преобразования типов (хотя преобразования типов лучше

использовать повсеместно, для сохранения совместимости).

Используемые расширения

----------------------------------------------------------------

Существует несколько дополнительных расширений языка Си, ко-

торые делают программу более читаемой. Эти расширения заменяют

некоторые анахронизмы ранних версий Си и позволяют вам писать бо-

лее эффективные, красивые и мобильные программы, а также повысить

производительность вашего труда.

Здесь мы представляем их краткий список.

Строковые литералы

-----------------------------------------------------------------

В классическом Си для получения длинной строковой переменной

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

новидности конкатенации. В современном Си вы можете просто раз-

бить большую строковую переменную на несколько строк таким обра-

зом:

main()

{

char *msg;

msg="Не так давно те, кто стал уже Си-хаккерами,"

"были вынуждены пускаться на различные ухищрения"

"для того, чтобы записать длинную стоку в"

"одну переменную. Но вам даже не нужно шевелить"

"мозгами, чтобы засунуть в переменную целое"

"стихотворение."

printf("%s",msg);

}

- 547,548 -

Шестнадцатиричная символьная константа

-----------------------------------------------------------------

В классическом Си ключевые последовательности, определяющие

конкретные символы в коде ASCII, представлены в восьмиричной сис-

теме счисления. Это обусловлено тем, что в начале развития машин

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

В настоящее время большинство компьютеров используют шест-

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

современный Си позволяет объявлять символьные константы в шест-

надцатиричной системе счисления. Основная форма шестнадцатиричной

константы имеет вид:

'\xDD'

где DD представляет одну или две шестнадцатиричные цифры (из

набора 0..9,A..F).

Эта ключевая последовательность может быть прямо присвоена

переменной типа char или может быть вложена в строку:

ch = '\x20'.

Типы со знаком.

-----------------------------------------------------------------

Классический Си подразумевает, что все int-типы являются ти-

пами со знаком, и потому включает модификатор типа unsigned чтобы

специфицировать обратное. По умолчанию переменные типа char счи-

таются signed (со знаком), что подразумевает изменение их значе-

ний от -128 до 127.

Однако, в настоящее время большинство компиляторов Си, реа-

лизованных для современных моделей компьютеров, воспринимают тип

данных char как unsigned. В связи с этим, для повышения мобиль-

ности программ на Турбо Си, в компилятор пакета включена опция,

позволяющая компилировать тип данных char как unsigned по умолча-

нию. В противном случае вы можете объявить символьную знаковую

переменную как signed char.

- 549,550 -

Ловушки в программировании на Си

-----------------------------------------------------------------

Существует несколько общих ошибок, допускаемых начинающими

программистами при программировании на Си. Далее предлагаются

несколько из них вместе с предложениями, как их избежать.

Маршрут MS DOS в строке Си

-----------------------------------------------------------------

Все знают, что обратный слеш (\) в MS DOS означает, что сле-

дующая далее строка есть имя каталога (справочника), однако в Си

слеш является ключевым символом в строке. Это противоречие явля-

ется одной из проблем, в тех случаях, когда вам необходимо ука-

зать маршрут доступа к файлу в строке Си. Например, вы набираете:

"c:\new\tools.c"

При этом вы ожидаете, что задается файл tools.c в каталоге

new на драйвере с. Это, однако, не совсем так. Вместо желаемого,

комбинация \n, являющаяся подстрокой строки Си, указывающей марш-

рут доступа к файлу, представляет собой ключевую последователь-

ность для перехода на новую строку (LF или символ перехода на но-

вую строку), а \t есть ни что иное как изображение символа

табуляции в Си-программе. Таким образом возникла ситуация, когда

в имя файла были встроены символы новой строки и табуляции. В ре-

зультате, DOS воспримет эту строку как неправильное имя файла,

так как имя файла не должно включать символов табуляции или новой

строки. Если же необходимость использования в имени файла комби-

наций символов, воспринимаемых как управляющие, велика, применяй-

те их экранирование путем удвоения обратного слеша для каждой из

встречающихся комбинаций управляющих символов.

Правильная строка (с учетом экранирования управляющих

последовательностей) будет иметь вид:

"c:\\new\\tools.c"

Неправильное употребление указателей

----------------------------------------------------------------

- 551,552 -

Использование указателей может быть наиболее трудно для по-

нимания начинающему программисту на Си. Когда надо использовать

указатели, а когда нет? Когда использовать косвенный оператор

(*)? Когда использовать адресный оператор (&)? И как избежать

ошибок операционной системы во время выполнения программы?

На все эти и многие другие вопросы ответ будет дан в этом

разделе.

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

-----------------------------------------------------------------

Одна серьезная опасность таится в присвоении значения по ад-

ресу, содержащемуся в указателе, без первоначального присвоения

адреса этому указателю.

Например:

main()

{

int *iptr;

*iptr = 421;

printf("*iptr = %d\n",*iptr);

}

Эта ловушка опасна тем, что программа, содержащая ее, может

быть "верна" и компилятор может не выдать никаких сообщений во

время компиляции такой программы. В примере, указанном выше, ука-

затель iptr имеет некоторый произвольный адрес, по которому запо-

минается значение 421. Эта программа настолько мала, что шанс что

-нибудь затереть в памяти с ее помощью ничтожно мал, однако в

больших программах возрастает вероятность разрушения других

данных, поскольку вполне возможно, что по адресу iptr уже

хранится другая информация. Если вы используете модель самой

маленькой памяти (tiny), в которой сегменты программы и данных

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

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

рубить сук на котором вы сами же сидите и внимательно пишите

программы, использующие указатели.

- 553,554 -

Строки

-----------------------------------------------------------------

Как мы уже говорили, строки можно объявить как указатели на

тип данных char или как массивы данных типа char. Речь шла о том,

что между ними существует лишь одно важное различие: если вы ис-

пользуете указатель на данные типа char, память для строки не ре-

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

вируется автоматически и переменная - имя массива - содержит

адрес начала зарезервированной области памяти.

Недостаточное понимание этой разницы может привести к двум

типам ошибок. Рассмотрим следующую программу:

main()

{

char *name;

char msg[10];

printf("Назовите свое имя.");

scanf("%s",name);

msg = "Здраствуйте, ";

printf("%s %s:",msg,name);

}

На первый взгляд все законно, немного неуклюже, но вполне

допустимо. Однако здесь допущены две ошибки.

Первая ошибка содержится в выражении:

scanf("%s",name).

Выражение само по себе законно и корректно. Поскольку name

является указателем на char, вам не нужно ставить перед ним ад-

ресный оператор (&). Однако память для name не зарезервирована;

строка, которую вы введете, будет записана по какому-то случайно-

му адресу, который окажется в name. Компилятор обнаружит это, но

поскольку эта ситуация не приведет к сбою выполнения программы

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

предупреждающее сообщение, но не ошибку.

"Possible use of 'name' before definition"

("Возможно использование 'name' до ее определения")

- 555,556 -

Вторая ошибка содержится в операторе msg = "Здравствуйте,".

Компилятор считает, что вы пытаетесь заменить значение msg на ад-

рес строковой константы "Здравствуйте,". Это сделать невозможно,

поскольку имена массивов являются константами, и не могут быть

модифицированы (как, например, 7 является константой и нельзя за-

писать "7 = i"). Компилятор выдаст вам сообщение об ошибке:

"Lvalue required."

("Использование константы недопустимо")

Каково решение этой проблемы? Простейший выход - изменить

способ описания переменных name и msg:

main()

{

char name[10];

char *msg;

printf("Назовите свое имя");

scanf("%s",name);

msg = "Здравствуйте,";

printf("%s%s",msg,name);

}

Эта программа безупречна. Переменной name выделяется память

независимо от памяти, выделяемой для строки, которую вы вводите,

тогда как в msg присваивается адрес строковой константы "Здравс-

твуйте,". Если, тем не менее, вы оставите старое описание, то вам

нужно изменить программу следующим образом:

#include <alloc.h>

main()

{

char *name;

char msg[10];

name = (char *) malloc (10);

printf("Назовите свое имя");

scanf("%s",name);

strcpy(msg,"Здраствуйте,");

printf("%s%s",msg,name);

}

Вызов функции malloc выделяет отдельно 10 байтов памяти и

- 557,558 -

присваивает адрес этого участка памяти name, решив нашу первую

проблему. Функция strcpy производит посимвольное копирование из

строковой константы string "Здравствуйте," в массив msg.

Разница между присваиванием (=) и равенством (==)

----------------------------------------------------------------

В языках Паскаль и Бейсик проверка на равенство производится

выражением

if (a = b).

Для Си эта конструкция допустима, но имеет несколько иное

значение. Посмотрите на этот фрагмент программы:

if (a = b) puts("Равно");

else puts("Не равно");

Если это программа на Паскале или Бейсике, то вы можете

предполагать, что будет напечатано "Равно", если a и b имеют оди-

наковое значение и "Не равно" в противном случае.

Иначе происходит с программой на Си, где выражение a = b оз-

начает "Присвоить значение b переменной a", и все выражение при-

нимает значение b. Поэтому во фрагменте, приведенном выше, прис-

воится значение b переменной a, а затем напечатается "Равно",

- 559,560 -

если b имеет нулевое значение, в противном случае - "Не равно".

Правильное решение следующее:

if (a == b) puts("Равно");

else puts("Не равно");

Пропуск break в операторе switch

----------------------------------------------------------------

Как вы помните, оператор break используется в операторе

switch в конце каждой помеченной альтернативы выбора. Пожалуйста,

не забывайте это. Если вы забудете поставить break в данную поме-

ченную альтернативу выбора, то операторы следующих альтернатив

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

- 561,562 -

Индексы массивов

-----------------------------------------------------------------

Не забудьте, что индекс массива начинается с элемента [0], а

не с элемента [1]. Наиболее распространенная ошибка может быть

проиллюстрирована на примере следующей программы:

main()

{

int list[100],i;

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

list[i] = i + 1;

}

Данная программа оставляет первый элемент list - list[0]

- неинициализированным, и записывает значение в несуществующий

элемент list - list[100] - возможно испортив при этом другие дан-

ные.

Правильная программа будет иметь следующий вид:

main()

{

int list[100],i;

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

list[i] = i+1;

}

- 563,564 -

Ошибки при передаче по адресу

-----------------------------------------------------------------

Посмотрите на следующую программу и найдите, что здесь неп-

равильно :

main()

{

int a,b,sum;

printf("Введите два значения:");

scanf("%d %d",a,b);

sum = a + b;

printf("Сумма значений равна: %d\n",sum0;

}

Нашли? Ошибка в операторе

scanf("%d %d",a,b);

Вспомните, что scanf требует от вас передачи адресов, а не

значений!

То же самое относится к любым функциям, содержащим в качест-

ве формальных параметров указатели. Программа, написанная выше,

оттранслируется и выполнится, но при этом scanf возьмет какие-то

случайные значения (мусор), находящиеся в a и b и использует их

как адреса, по которым будут записаны введенные вами значения.

Правильно этот оператор необходимо записать так:

scanf("%d %d", &a, &b);

Здесь функции scanf передаются адреса a и b, и введенные

значения правильно запоминаются в этих переменных по их адресам.

Та же неприятность может случиться с вашей собственноручно

написанной функцией. Помните функцию swap, которую мы определили

в параграфе об указателях?

Что произойдет, если вы вызовете ее следующим образом:

main()

{

- 565,566 -

int i,j;

i = 421;

j = 53;

printf("До обработки: i = %fd j = %fd\n",i,j);

swap(i,j);

printf("После обработки: i = %fd j = %fd\n",i,j);

}

Переменные i и j будут иметь одни и те же значения как до,

так и после их обработки путем обращения к функции swap; однако

значения адресов чисел 421 и 53 будут переставлены, что породит

некоторые хитрые и труднообнаружимые ошибки. Как избежать этого?

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

функций.

И действительно, вы получили бы ошибку при выполнении main,

если бы swap была описана так, как в этой главе. Если же вы опре-

делите ее таким образом, как представлено далее, то программа

выполнится без ошибок:

void swap(a,b)

int *a,*b;

{

...

}

Вынесение описания переменных a и b из скобок отключает

контроль над ошибками, проводимый в противном случае; это являет-

ся лучшим доводом в пользу того, чтобы не использовать классичес-

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

- 567,568 -

В добрый путь

----------------------------------------------------------------

Как и говорилось в начале предыдудущей главы, мы не можем

дать вам полное представление о Си только в двух главах. Но мы

предложили вам оптимальный вариант знакомства с этим языком прог-

раммирования. Что вам теперь делать дальше - ответ в предостав-

ленных вам примерах программ: компилируйте их, выполняйте, и (что

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

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

Удачи вам, счастливого пути в познании тайн

программирования на Си!