Ñàíêò-Ïåòåðáóðãñêèé ãîñóäàðñòâåííûé óíèâåðñèòåò àýðîêîñìè÷åñêîãî ïðèáîðîñòðîåíèÿ
Н.В. Кучин, М.М.Павлова
УДК 519. 682 (075) ББК 32. 973 – 18.1 К95 Кучин Н. В., Павлова М. М. К95 Основы программирования на языке СИ: Учеб. пособие / СПбГУАП. СПб., 2001. 86 с. Изложены и систематизированы базовые элементы языка СИ. Рассмотрены конструкции языка СИ в объеме, позволяющем составлять программы, как для простых, так и достаточно сложных задач. Приведены примеры для отработки навыков составления программ. Пособие предназначено для студентов специальности «Вычислительные машины, комплексы, системы и сети», направления «Информатика и вычислительная техника», кроме того может быть использовано для студентов всех специальностей университета, изучающих курсы «Информатика» и «Программирование»
Рецензенты: кафедра информатики и прикладной математики СПбГИТМО (ТУ) (кандидат технических наук доцент Т. А. Павловская); профессор кафедры радиолокационных систем государственного университета телекоммуникаций им. М. А. Бонч-Бруевича доктор технических наук В. В. Волков
Óòâåðæäåíî ðåäàêöèîííî-èçäàòåëüñêèì ñîâåòîì óíèâåðñèòåòà â êà÷åñòâå ó÷åáíîãî ïîñîáèÿ
ПРЕДИСЛОВИЕ Язык C был создан в начале 70-х годов двадцатого столетия и в настоящее время является основой для создания значительной части системных программ и приложений. Он эффективен как для решения задач системного программирования, так и для создания прикладных программ. Среди преимуществ языка C можно отметить переносимость программ, написанных на языке С, на компьютеры различной архитектуры и из одной операционной системы в другую, лаконичность записи алгоритмов, логическую стройность, возможность получить эффективный код программ, сравнимых по скорости с программами, написанными на ассемблере. Удобство языка С основано на том, что он является одновременно и языком высокого уровня, имеющим полный набор конструкций структурного программирования, поддерживающим модульность, блочную структуру программ, возможность раздельной компиляции модулей. В то же самое время язык С имеет набор низкоуровневых средств, позволяющих иметь удобный доступ к аппаратным средствам компьютера, в частности, позволяющих добраться до каждого бита памяти. Гибкость и универсальность языка С обеспечивает его широкое распространение. Учебное пособие ориентировано на начинающих программистов, имеющих первоначальные понятия об основах алгоритмизации на уровне курса информатики. Каждый раздел пособия снабжен соответствующими примерами, которые представляют собой программы, написанные на языке С, или отдельные конструкции таких программ. Все приводимые примеры используют стандарт ANSI языка С и ориентируются на трансляторы, созданные фирмой Borland International Inc., как самые распространенные. Усвоение учебного материала, представленного в данном пособии, позволит приобрести устойчивые навыки правильного написания программ на языке С, а также значительно облегчить изучение объектно-ориентированного языка – С++.
1. ОСНОВНЫЕ ПОНЯТИЯ ЯЗЫКА С 1.1. Алфавит, идентификаторы, ключевые слова, комментарии Алфавитом языка называется совокупность символов, используемых в языке. Язык C различают, в отличие от многих других языков программирования, прописные и строчные буквы. Язык C, как говорят, является чувствительным к регистру. В языке С имена COLOR, color, и Color определяют три различные имени переменных. Поэтому при написании программ необходимо быть внимательным при использовании регистров. Обычно используется следующее соглашение – имена переменных содержат только строчные буквы (нижний регистр), константы и макросы – прописные буквы (верхний регистр). В именах переменных можно использовать символ подчеркивания. Обычно с символа подчеркивания начинаются имена системных переменных и констант. В библиотечных функциях также часто используются имена, начинающиеся с символа подчеркивания. Поэтому лучше не использовать этот символ как первый при написании своих собственных переменных, что позволит избежать возможных конфликтов. Идентификаторы в языке программирования используются для обозначения имен переменных, функций и меток, применяемых в программе. Идентификатором может быть произвольная последовательность латинских букв (прописных и строчных), цифр и символа подчеркивания, которая начинается с буквы или символа подчеркивания. В языке C идентификатор может состоять из произвольного числа символов, однако два идентификатора считаются различными, если у них различаются первые 32 символа. В языке C некоторые идентификаторы употребляются как служебные слова, которые имеют специальное значение для компилятора. Их употребление строго определено, и эти слова не могут использоваться иначе. Ключевыми словами стандарта ANSI языка C являются 4
Часть символов язык C рассматривает как пробельные символы. Это не только символ пробела ‘ ‘, но символы табуляции, символы перевода строки, возврата каретки, символ перевода страницы. Комментарий – это часть программы, которая игнорируется компилятором и служит для удобочитаемости текста программы. В процессе компиляции комментарий заменяется пробелом, следовательно, комментарий может располагаться в любом месте программы, где допустимо использование пробела. Комментарием в языке C является любая последовательность символов, заключенная между парой символов /* и */. 1.2. Примеры простых программ Рассмотрим следующую программу: # include < stdio.h > /* Пример 1 */ main() { int year; year = 2000; printf(“Сейчас %d год\n”,year); } Первая строка # include < stdio.h > сообщает о необходимости подключения файла stdio.h. Этот файл содержит информацию, необходимую для правильного выполнения функций библиотеки ввода/вывода языка С. Язык C предусматривает использование таких файлов, которые называются заголовочными. В данном случае использование файла stdio.h необходимо, так как в этом файле находится информация о функции printf () , которая используется в нашей программе. Вторая строка /* Пример 1 */ является комментарием. 5
Строка main() определяет имя функции. Любая программа на языке C включает одну или более функций. Выполнение программы начинается с функции main(). Поэтому каждая программа на языке C должна содержать эту функцию. Следующая строка содержит открывающуюся фигурную скобку, обозначающую начало тела функции main(). Фигурные скобки в языке C всегда используются парами (открывающая и закрывающая). Закрывающая скобка встретится в данной программе далее. Строка int year; объявляет переменную, названную year, и сообщает компилятору, что это переменная целого типа. В языке C все переменные должны быть объявлены прежде, чем они будут использованы. Процесс объявления переменных включает в себя определение имени и указания типа переменных. Строка year = 2000; является оператором присваивания. Здесь переменной с именем year присваивается значение 2000. Заметим, что в языке C в операторе присваивания используется просто знак равенства. Все операторы в языке C заканчиваются символом “ точка с запятой”. Строка printf(“Сейчас %d год\n”,year); является вызовом стандартной функции printf(), которая выводит на экран некоторую информацию. Эта строка состоит из двух частей: имени функции printf() и двух ее аргументов “Сейчас %d год\n” и year, разделенных запятой. Функция printf() является универсальной функцией форматного вывода. Для вызова функции нужно правильно написать имя функции и в скобках указать необходимые фактические аргументы. Первый аргумент функции printf() – это строка в кавычках “Сейчас %d год\n”, которую называют управляющей строкой. Эта строка может содержать любые символы или спецификации формата, начинающиеся с символа %. Обычные символы просто отображаются на экран в том порядке, в котором они следуют. Спецификация формата, начинающаяся с символа %d указывает формат, в котором будет выводится значение переменной year, являющейся вторым аргументом функции printf(). Спецификация %d указывает, что будет выводится целое число в десятичной записи. Комбинация символов ‘\n’ сообщает функции printf() о необходимости перехода на новую строку. Этот символ называется символом новой строки. Последняя строка программы содержит фигурную закрывающую скобку. Она обозначает конец функции main(). 6
Рассмотрим второй пример, в котором будет использоваться ввод данных с клавиатуры. Для этого будет использоваться библиотечная функция scanf(), которая позволяет вводить информацию с клавиатуры во время выполнения программы # include < stdio.h > /* Пример 2: вычисление длины окружности */ main() { int radius; float length; printf(“ Введите значение радиуса: \n”); scanf(“%d”,&radius); length = 3.1415*2*radius; printf(“Радиус-%d\n,Длина- %f\n”,radius,length); } В этой программе по сравнению с предыдущей появились новые моменты. Во-первых, объявлены переменные двух разных типов: radius – типа целое(int); length – типа с плавающей запятой (float), содержащую дробную часть. Во-вторых, используется функция scanf() для ввода с клавиатуры значения радиуса окружности. Первый аргумент функции scanf() “%d” указывает, что будет вводится целое десятичное число. Второй аргумент – имя переменной, которой будет присвоено введенное значение. Символ & (амперсанд) перед именем переменной radius необходим для правильной работы функции scanf(). Более подробно использование этого символа будет обсуждаться позднее. В следующей строке программы целые числа 2 и radius умножаются на число с плавающей запятой 3.1415 и результат присваивается переменной типа float. Язык C допускает использование в выражениях переменных разных типов. Для вывода результатов используется функция printf(), которая содержит 4 аргумента. Спецификатор формата %f используется для печати значения переменной length типа float. В рассмотренном примере длина окружности вычисляется только для целых радиусов. Для того чтобы программа могла вычислять длину окружности для любых радиусов, необходимо объявить переменную radius как float, а в функции scanf() использовать спецификатор “%f”. 7
1.3. Определение некоторых понятий Исходный текст (source cod) – текст программы на языке программирования. Объектный код (object cod) – текст программы на машинном языке, который не может выполняться компьютером. Получается после компиляции исходного текста файла или программы. Компоновщик (linker) – программа, строящая выполняемый модуль из объектных модулей. Эта программа собирает откомпилированный текст программы и функций из стандартных библиотек языка C в одну выполняемую программу. Библиотека (library) – набор функций, включая стандартные, предопределенных переменных и констант, которые могут быть использованы в программе и хранятся в откомпилированном виде. Время компиляции (compiler time) – период, во время которого происходит компиляция программы. Во время компиляции обнаруживаются синтаксические ошибки, сделанные при составлении текста программы. Время выполнения (run time) – период, во время которого происходит выполнение программы.
2. ПЕРЕМЕННЫЕ, КОНСТАНТЫ, ОПЕРАЦИИ И ВЫРАЖЕНИЯ 2.1. Базовые типы данных и объявление переменных В языке C все переменные должны быть объявлены до их использования. В нем определены 5 типов данных, которые считаются базовыми: char – символьные; int – целые; float – с плавающей точкой; double – с плавающей точкой двойной длины; void – пустой, не имеющий значения. Типы char и int являются целыми типами и предназначены для хранения целых чисел. Хотя тип char по названию символьная переменная, любой символ в компьютере связан с целым числом – кодом этого символа в таблице ASCII. Сам символ нам необходим, когда информация выводится на экран или на принтер или, наоборот, вводится с клавиатуры. Подобные преобразования символа в код и обратно производятся автоматически. Тип char по умолчанию является знаковым типом, однако, настройкой опций интегрированной среды можно установить по умолчанию беззнаковый тип char. Тип int является всегда знаковым, также как и типы float и double. Переменные типа double и float являются числами с плавающей точкой. Ключевое слово void привнесено из стандарта ANSI для языка C++. Нельзя создать переменную типа void. Тем не менее введение такого типа оказалось необходимым, что будет показано в дальнейшем. На основе этих пяти типов строятся другие типы данных. Простейшим приемом построения других типов является использование модификаторов, которые ставятся перед соответствующим типом. В стандарте ANSI языка C такими модификаторами являются следующие зарезервированные слова: 9
signed – знаковый; unsigned – беззнаковый; long – длинный; short – короткий. Модификаторы signed и unsigned могут применяться к типам char и int. Модификаторы short и long могут применяться к типу int. Модификатор long может применяться также к типу double. Модификаторы signed и unsigned могут комбинироваться с модификаторами short и long применительно к типу int. В табл. 2.1 приведены все возможные типы данных с различными модификациями модификаторов. Таблица 2.1 Типы данных и модификаторы Тип
char unsigned char signed char int unsigned int signed int short int unsigned short int signed short int long int signed long int unsigned long int float double long double
–128 до 127 0 до 255 –128 до 127 –32768 до 32767 0 до 65535 –32768 до 32767 –32768 до 32767 0 до 65535 –32768 до 32768 –2147483648 до 2147483647 –2147483648 до 2147483647 0 до 4294967295 3.4E–38 до 3.4E+38 1.7E–308 до 1.7E+308 3.4E–4932 до 3.4E+4932
Запись 3.4E-38 соответствует числу 3.4*10–38, это так называемый научный формат записи числа с плавающей точкой. Различие между целыми числами со знаком и целыми числами без знака состоит в том, как интерпретируется старший бит целого числа. Если старший бит целого числа со знаком равен нулю, – число положи10
тельное, если же старший бит равен единице, – число отрицательное. Например, целое число +5 типа int будет храниться в памяти компьютера в виде 0000000000000101 Если объявлено целое отрицательное число, то компилятор генерирует обратный код. Чтобы получить число –5 надо поменять значения всех битов на обратные, т.е. 0 заменить на 1, 1 заменить на 0 и прибавить к младшему биту 1. Число –5 в двоичной записи в обратном коде будет иметь вид 1111111111111011 2.2. Основная форма объявления переменных Здесь тип должен быть одним из существующих в C типов переменных, а список переменных может состоять из одной или нескольких переменных, разделенных запятыми. При объявлении переменных компилятор выделяет место в памяти компьютера, необходимое для размещения переменной указанного типа. Примеры объявления переменных int a,b,c; float radius,length; unsigned char ch1,ch2; long double integral; Очень важное значение имеет вопрос о месте объявления переменной в программе. Правило, определяющее место объявления переменной в программе, называется правилом видимости. В языке C могут быть три места, где переменная может быть объявлена. Во-первых, вне каких либо функций, в том числе и main(). Такая переменная называется глобальной и может использоваться в любом месте программы ( за исключением глобальных статических переменных, речь о которых далее). Во-вторых, переменная может быть объявлена внутри блока, в том числе внутри тела функции. Такая переменная называется локальной и может использоваться только внутри этого блока. Такая переменная неизвестна вне этого блока. Кроме того, переменная может быть объявлена как формальный параметр функции. Переменная, объявленная как формальный 11
параметр функции, используется для передачи информации этой функции, а также может рассматриваться как локальная переменная функции. Рассмотрим пример объявления переменных в разных местах программы #include
/* Пример 3 */ #include <stdio.h> /* объявляем переменные в разных местах программы */ char ch; /* объявление глобальной переменной*/ main() { int n; /* объявление локальной переменной функции main() */ printf(“ Введите символ “); ch=getche(); /* использование глобальной переменной */ printf(“ Введите количество символов в строке“); scanf(%d,&n) /* использование локальной переменной*/ print_str(n); /* обращение к функции посимвольной печати строки*/ } print_str(int m) /* заголовок функции print_str() с объявлением формального параметра m*/ { int j; /* объявление локальной переменной для функции print_str*/ for (j=0; j<m; j++) /* использование локальной переменной j*/ printf(“%c\n”,ch); /* использование глобальной переменной ch*/ } При написании программ необходимо помнить следующие правила: – две глобальные переменные не могут иметь одинаковые имена; – локальная переменная одной функции может иметь такое же имя, как локальная переменная другой функции (или формальный параметр другой функции); – две локальные переменные в одном блоке не могут иметь одинаковые имена, в том числе формальный параметр функции не должен совпадать с локальным параметром, объявленным в функции.
12
2.3. Константы В языке C константы представляют фиксированные величины, которые не могут быть изменены в программе. Константы могут быть любого базового типа данных. Примеры констант char int unsigned int long int short int float double
‘a’, ‘\n’, ‘8’ 1, 134, -580 52500 87000, -37, 7L 11, 13, -128 133.34, 3.36E-6, 5E+5 133.34, 133340, -2.789
Правила определения типа констант следующие. Целая константа относится к типу int, если эта константа входит в интервал значений типа int. Если эта константа не ходит в интервал значений типа int, например 37750, то она считается константой типа unsigned. Если же константа не входит в интервал изменения unsigned, она считается константой типа long. Константа с десятичной точкой считается константой типа double, если она помещается в соответствующий интервал измерения. Для явного задания типа констант используется механизм суффиксов. В качестве суффиксов целочисленных констант используются u,l,h,U,L,H. Для чисел с плавающей точкой – l,L,f,F. Например 13h, 35H 25L, -223l 87lu 88Lu 89ul 55uh unsigned 27.43f 7.7E-6F 1.41l 3.2E+12L
short int long int unsigned long short float double
В программировании важную роль играют восьмеричные и шестнадцатеричные константы. Перед шестнадцатеричной константой ставится пара 0x. Восьмеричная константа всегда начинается с нуля. Шестнадцатеричные и восьмеричные константы всегда являются беззнаковыми. В качестве цифр восьмеричных констант используются символы – 0,1,2,3,4,5,6,7. В качестве цифр шестнадцатеричных кон13
стант используются символы – 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F. В табл. 2.2 представлено соответствие между десятичными и шестнадцатеричными числами, а также записями восьмеричных и шестнадцатеричных констант. Таблица 2.2 Числа и константы Десятичное число
Шестнадцатеричное число
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 1 2 3 4 5 6 7 8 9 A B C D E F
Двоичная Восьмеричная Шестнадцатеричная запись числа константа константа
0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
0×0 0×1 0×2 0×3 0×4 0×5 0×6 0×7 0×8 0×9 0×A 0×B 0×C 0×D 0×E 0×F
00 01 02 03 04 05 06 07 10 11 12 13 14 15 16 17
Для представления в двоичном виде шестнадцатеричного числа надо просто заменить двоичной записью каждую цифру этого числа. Например, число 0xAC1F представимо в виде A 1010
C 1100
1 0001
F 1111
Строковые константы (strings) также играют в языке C важную роль. Строковая константа (строка) представляет собой набор символов, заключенный в двойные кавычки. Особенностью представления таких констант в памяти компьютера является то, что необходи14
мо отводить на один байт больше, чем требуется для размещения всех символов строки. Этот последний байт заполняется нулевым значением, называется нулевым байтом и имеет специальное обозначение ‘\0’. Нельзя путать строковые константы с символьными константами. Так “b” – это строковая константа, содержащая одну букву, а ‘b’ – символьная константа, или просто символ. Отличие “b” от ‘b’ в том, что строка “b” содержит еще один символ ‘\0’ в конце строки, “b” занимает в памяти 2 байта, в то время как ‘b’ – только один байт. В языке C есть символьные константы, которые не соответствуют никакому из печатных символов. Так, в коде ASCII символы с номерами от нуля до 31 являются управляющими символами, которые нельзя ввести с клавиатуры. Для использования таких символов вводятся так называемые управляющие константы. Управляющие символы представлены в табл. 2.3: Таблица 2.3 Управляющие символы Управляющий символ
\b \f \n \r \t \v \" \' \\ \0 \a \N \xN \?
Значение
BS, забой Новая страница, перевод страницы Новая строка, перевод строки Возврат каретки Горизонтальная табуляция Вертикальная табуляция Двойная кавычка Апостроф Обратная косая черта Нулевой символ, нулевой байт Сигнал Восьмеричная константа Шестнадцатеричная константа Знак вопроса
Если за символом обратной черты следует символ не из этой таблицы, то эта пара воспринимается просто как соответствующий символ. 15
2.4. Символьные переменные и строки Символьная переменная – это величина размером в 1 байт, которая используется для представления литер и целых чисел в диапазоне от 0 до 255 или от –128 до 127, в зависимости от того, знаковая переменная или беззнаковая. Символьные константы заключаются в одинарные кавычки. Примеры символьных констант: ‘d’, ‘+’, ‘8’. Приведем пример программы с использованием символьных переменных и констант # include < stdio.h > /* Пример 4 */ main() { char ch; ch=’c’; printf(“%c”,ch); ch=’+’; printf(“%c%c”,ch,ch); } В функции prinf() появилась новая спецификация – %c. В таком формате печатается символ. Этот же формат можно использовать в функции scanf() для ввода символа с клавиатуры. В языке С в стандартной библиотеке ввода/вывода есть специальная функция getche(). Эта функция ожидает, пока не будет нажата какая-либо клавиша клавиатуры, и затем вводит код этой клавиши. Рассмотрим пример программы, использующей указанную функцию # include < conio.h > # include < stdio.h > /* Пример 5 */ main() { char ch; printf(“Нажмите любую клавишу”); ch=getche(); if ( ch == ‘a’) printf(“Вы нажали клавишу a\n”); printf(“ Вы нажали клавишу %c”,ch); } 16
В языке C строка – это массив символов, заканчивающихся нулевым байтом. В языке C нет стандартного типа строка (в отличие от языка Pascal) и строка объявляется как одномерный массив символов, но для работы с массивом символов как со строкой имеется набор библиотечных функций. Одномерный массив – это упорядоченная последовательность данных одного типа. В программе одномерный массив символов объявляется как char str[80]; В этом описании char – тип элементов массива, str – имя массива, в квадратных скобках указан размер массива – 80. Для обращения к отдельному элементу массива нужно указать после имени массива номер элемента в квадратных скобках, например – str[12]. В языке С все элементы массива нумеруются начиная с нуля, т. е. str[0] – первый элемент массива, str[2] – второй элемент массива, str[79] – 80-й элемент массива. Следует помнить, что в С строка – это массив символов, заканчивающихся нулевым байтом, поэтому при объявлении массива, с которым необходимо работать как со строкой, следует зарезервировать место под нулевой байт. Например, если слово english – это символьная строка, то под нее нужно зарезервировать массив из восьми символов: семь для букв, один последний символ для нулевого байта. Для чтения строки с клавиатуры необходимо создать символьный массив и затем использовать библиотечную функцию gets(). В качестве аргумента функции gets() используется имя массива, куда вводится строка. Функция gets() читает символы с клавиатуры до тех пор, пока не будет нажата клавиша Enter. Нажатие этой клавиши устанавливает в конец строки нулевой байт. Пример программы, где используется ввод строки с клавиатуры # include < stdio.h > /* Пример 6 */ main() { char str[80]; printf(“ Введите ваше имя “); gets(str); printf(“ Ваше имя - %s”,str); } 17
В данном примере используется спецификатор %s. Он предназначен для ввода/вывода строк. 2.5. Инициализация переменных После того как переменная объявлена, ей в процессе выполнения программы должно быть присвоено значение. Язык С предоставляет возможность программисту присвоить значение переменной одновременно с процессом ее объявления. Основная форма инициализации переменной имеет вид тип имя переменной = константное выражение; Например: int a=230; char c=’c’, ch=’0’; Объявление переменной с одновременной инициализацией ее значения приводит к тому, что одновременно с выделением памяти в эту память записывается значение инициализации. Глобальные или статические переменные всегда инициализируются либо нулем, либо значением инициализатора. Локальные переменные остаются неопределенными до первого присвоения им значения. Глобальные переменные инициализируются только один раз в начале выполнения программы. Локальные переменные инициализируются при каждом вызове функции. В стандарте ANSI для С инициализировать можно только константным выражением. В реализации языка Borland C инициализировать можно не только константой, но и выражением с использованием значений переменных, которые были ранее определены (динамическая инициализация). 2.6. Операции В состав языка С входит большое число разнообразных операций. Знак операции в языке С – это некоторый символ или комбинация символов, которые сообщают компилятору о необходимости выполнить определенные арифметические, логические или другие действия. Каждая операция, кроме своего содержательного смысла, также определяется рядом характеристик: типы и число операндов, порядок своего выполнения, приоритет выполнения по отношению к другим операциям. Рассмотрим различные виды операций. Арифметические операции. К арифметическим операциям относятся: 18
- вычитание и унарный минус; + сложение; * умножение; / деление; % деление по модулю; ++ увеличение на единицу (инкрементация ); - - уменьшение на единицу (декрементация ). Операции сложения, вычитания, умножения и деления действуют так же, как и в большинстве других языках программирования. Они могут применяться ко всем встроенным типам данных. Операции выполняются слева направо, т. е. сначала вычисляется выражение левого операнда, затем выражение, стоящее справа от знака операции. Если операнды имеют один тип, то результат арифметической операции имеет тот же тип. Поэтому, когда операция деления / применяется к целым переменным или символьным переменным, остаток отбрасывается. Операция деление по модулю % дает остаток от целочисленного деления. Такая операция может применяться только к целочисленным переменным. Язык С предоставляет программисту еще две очень полезные и специфические операции – унарные операции ++ и —. Операция ++ прибавляет единицу к операнду, операция – вычитает единицу из операнда. Обе операции могут следовать перед операндом или после операнда (префиксная и постфиксная формы). Три написанные ниже оператора дают один и тот же результат, но имеют различие при использовании в выражениях a=a+1; ++a; a++. Рассмотрим программу, позволяющую понять это различие # include < stdio.h > /* Пример 7 */ main() { int a=10; int b=70; a++; ++b; printf(“a=%d b=%d\n”,a,b); printf(“a=%d b=%d\n”,a++,++b); } 19
Результатом выполнения этой программы будет следующее: a=11, b=71; a=11, b=72. Значение переменной a не изменилось при втором обращении к функции printf(), а значение переменной b увеличилось на единицу. На самом деле значение переменной a также увеличилось на единицу, но уже после выхода из функции printf(). Различие в использовании префиксной и постфиксной форм состоит в следующем: a++ – значение переменной a сначала используется в выражении, и лишь затем увеличивается на единицу; ++a – переменная a сначала увеличивается на единицу, а затем ее значение используется в выражении. Старшинство арифметических операций следующее: ++, — - (унарный минус) *, /, % +, Операции, одинаковые по старшинству, выполняются в порядке слева направо. Порядок следования операций можно изменить, используя в выражениях круглые скобки. Операции отношения и логические операции. Операции отношения используются для сравнения. Полный список операций отношения в языке С следующий: < меньше, <= меньше или равно, > больше, >= больше или равно, == равно, != не равно. Также имеется три логические операции: && и (AND) || или (OR) ! не (NOT). Операции отношения используются в условных выражениях. Примеры условных выражений x<1, 200>=199, ‘c’==’C’ 20
Каждое условное выражение проверяется: истинно оно или ложно, т. е. каждое условное выражение, при своем выполнении, принимает значение “истинно” (“true”) или “ ложно” (“false”). В языке С нет логического типа данных. Поэтому результатом логического выражения является целочисленное значение. В языке С “ истинно” – это ненулевая величина, “ ложно” – это нуль. В большинстве случаев в качестве ненулевой величины используется единица. Рассмотрим пример #include < stdio.h > /* Пример 8 */ main() { int tr, fal; tr=(99<100); /* выражение истинно */ fal=(100>101); /* выражение ложно */ printf(“true - %d false - %d\n”,tr,fal); } Логические операции в языке С соответствуют классическим логическим операциям AND(&&), OR(||) и NOT(!), а их результат приводится в табл. 2.4 . Таблица 2.4 Логические операции и их результат X
Y
X AND Y
X OR Y
NOT X
X XOR Y
0 1 X 0 1
0 0 Y 1 1
0 0 X AND Y 0 1
0 1 X OR Y 1 1
1 0 NOT X 1 0
0 1 X XOR Y 1 0
Операция XOR называется операцией “исключающее или”. В языке С нет знака логической операции XOR, хотя она может быть реализована с помощью операций AND, OR, NOT. Далее будут рассмотрены побитовые операции, среди которых операция “исключающее или” уже есть. Операции отношения и логические операции имеют более низкий приоритет, чем арифметические операции. Старшинство логических операций и операций отношения следующие: 21
! > == && ||
< !=
>=
<=
В логических выражениях также можно использовать круглые скобки, которые имеют наивысший приоритет. Кроме того, использование круглых скобок позволяет сделать логические выражения более понятными и удобными при чтении текста программ. Условные и логические выражения, чаще всего, используются в управляющих операторах языка С, речь о которых пойдет далее. Операция присваивания. Операция присваивания обозначается знаком = , и используется для реализации оператора присваивания. В отличие от других языков в языке С оператор присваивания может использоваться “внутри” различных управляющих операторов, речь о которых пойдет далее. В фрагменте If ((c=a-b)<0) printf (“число а меньше чем b“) сначала вычисляется величина a-b, которая присваивается переменной c, затем сравнивается ее значение с нулем. Кроме того, в языке С имется возможность многократного присваивания, например: a=b=c=x+y. Здесь сначала вычисляется значение x+y, затем оно присваивается переменной c, потом b, и лишь затем a. В левой части оператора присваивания должно стоять выражение, которому можно присвоить значение. Такое выражение в языке С, например просто переменная, называется величиной lvalue. Выражение 3 = 3 ошибочно, так как константе нельзя присвоить никакое значение: константа не является величиной lvalue. В языке С имеются дополнительные операции присваивания +=, -=, /=, *=, %=. Вместо выражения a=a+7 можно использовать выражение a+=7. Здесь += аддитивная операция присваивания, в результате выполнения которой величина, стоящая справа, прибавляется к значению переменной, стоящей слева. Аналогично выполняются остальные дополнительные операции присваивания. Дополнительные операции присваивания имеют тот же приоритет, что и операция =, т. е. ниже, чем приоритет арифметических операций. Необходимо отметить, что операция a+=7 выполняется быстрее, чем операция а=а+7. 22
Поразрядные операции (побитовые операции ). Поразрядные операции проводятся с любыми целочисленными переменными и константами. Нельзя использовать эти операции с переменными типа float, double, long double. Результатом побитовой операции будет целочисленное значение. Поразрядными (побитовыми) операциями являются & AND, | OR, ^ XOR, ~ NOT, << сдвиг влево, >> сдвиг вправо. Операции AND, OR, NOT и XOR нам уже известны, они аналогичны соответствующим логическим операциям. Только в этом случае сравниваются значения соответствующей пары битов, а не значения выражений. Поразрядные операции позволяют обеспечить доступ к отдельным битам информации и поэтому часто используются в программах кодирования и декодирования информации, различных драйверах. При выполнении поразрядной операции над двумя переменными, например типа char, операция производится над каждой парой соответствующих разрядов(битов). Отличие поразрядных операций от логических и операций отношения состоит в том, что логические операции и операции отношения всегда в результате дают 0 или 1. Для поразрядных операций это не так. Приведем ряд примеров: 1) ‘A’ 127 ‘A’&127
11000001 01111111 01000001
2) ‘A’ 11000001 128 10000000 ‘A’| 128 11000001
Поразрядные операции удобны для хранения информации в сжатом виде, например о состоянии некоторого объекта. Значение каждого бита ( 0 или 1) можно интерпретировать как “отсутствие/присутствие” или “включен/выключен”. В каждом байте можно хранить 8 таких флагов. Если переменная ch является хранилищем таких флагов, то определить значение четвертого бита можно следующим образом: If ( ch & 8 ) printf(“4 бит содержит 1”); 23
Эта проверка основывается на двоичном представлении числа 8=00001000. Операции сдвига << и >> применимы только к целочисленным переменным. В результате применения этих операций сдвигаются все биты левого операнда на число позиций, определяемого выражением справа от знака операции влево или вправо. Форма этих операций следующая: value << число позиций value >> число позиций. Пример: двоичное представление числа x=9: 00001001, тогда x=9<<3 01001000 x=9>>3 00000001 x=9>>5 00000000 Применение операций сдвига может приводить к потере младших или старших байтов. Применение операций << и >> по очереди к одной и той же переменной может изменить значение этой переменной из-за потери разрядов. Поразрядные операции порождают еще несколько сложных операций присваивания: |=, &=, ^=, <<=, >>=. Операции ( ) и [ ]. В языке С круглые и квадратные скобки также рассматриваются как операции. Причем эти операции имеют высший приоритет. Операция условие? Операция условие – единственная операция языка С, имеющая три операнда. Эта операция имеет вид ( выр1 )?(выр2):(выр3) Вычисляется выражение (выр1). Если это выражение имеет ненулевое значение, то вычисляется выражение (выр2), и оно будет результатом выполнением операции. Если значение выражения (выр1) равно нулю, то вычисляется значение выражения (выр3) и его значение будет результатом операции. В любом случае вычисляется только одно из выражений: (выр2) и (выр3). Например, операцию условие удобно применять для нахождения наибольшего из двух чисел a и b max=(a>b)?a:b; или для нахождения абсолютной величины числа a abs=(a>0)?a:-a; Если второй и третий операнды являются величинами типа lvalue, т. е. могут стоять в левой части операции присваивания, то и результат операции условие является величиной типа lvalue. С помощью этой 24
операции можно просто решить такую задачу: найти наибольшее из двух чисел заменить его на единицу: (a>b)?a:b=1; Операция запятая. Операция запятая имеет самый низкий приоритет из всех операций языка С. Она выполняется слева направо, и ее значением является значение правого операнда. В выражении (выр1), (выр2) сначала вычисляется значение (выр1), а затем – значение (выр2). Это значение и будет результатом операции. Операция sizeof.. Операция sizeof имеет две формы: sizeof(тип) или sizeof(выражение). Результатом этой операции является целочисленная величина типа или выражения в байтах. При использовании второй формы значение самого выражения не вычисляется, а лишь определяется его тип. Примеры использования: sizeof( double ) или sizeof( a ). В качестве типа в операции sizeof не может использоваться тип void. В дальнейшем покажем, что ряд операций в языке С имеют двойное назначение, например операция &. Кроме того операции . и -> рассмотрим позднее. 2.7. Выражения Выражения в языке С – это некоторая допустимая комбинация переменных, констант и операций. Каждое выражение принимает какое-либо значение. В выражениях языка С допустимо смешение переменных разного типа. Правила языка С, которые используются для автоматического приведения типов при вычислении арифметического выражения, следующие: 1. Все переменные типа char и short int преобразуются в int, все переменные типа float преобразуются в double. 2. Для любой пары операндов, если один из операндов long double, то и другой преобразуется в long double; если один из операндов double, то и другой преобразуется в double; если один из операндов long, то и другой преобразуется в long; если один из операндов unsigned, то и другой преобразуется в unsigned. 3. В операторе присваивания конечный результат приводится к типу переменной в левой части оператора присваивания, при этом тип может как повышаться так и понижаться. Тип результата вычисления выражения можно изменить, используя конструкцию “приведение”, имеющую следующий вид: ( тип ) выражение 25
Здесь “тип” – один из стандартных типов данных. Например, если необходимо сделать результат деления переменной a типа int на 2 типом float, надо записать ( float ) a/2. Значение выражения (float) (a/2) будет другим, чем в первом случае. Это связано с тем, что в первом случае приведение типа применялось к переменной а, а во втором случае – к результату вычисления выражения в круглых скобках а/2. Пробелы и круглые скобки в выражениях расставляются так как необходимо программисту. При компиляции лишние пробелы просто игнорируются.
26
3. ОПЕРАТОРЫ 3.1. Условный оператор Основная форма условного оператора выглядит следующим образом: if ( условие ) оператор; else оператор; Если значение условия “ истинно”, то выполняется оператор (им может быть составной оператор), следующий за условием. Если же условие принимает значение “ ложно” то выполняется оператор, следующий за словом else. В записи оператора if вторая часть (т. е. оператор else) может отсутствовать. Тогда, если условие принимает значение “ ложно”, выполняется следующий оператор программы. В качестве условия может стоять произвольное выражение. В операторе if лишь проверяется, является ли значение этого выражения ненулевым (истинным) или нулевым (ложным). Рассмотрим пример программы определения знака вводимого с клавиатуры вещественного числа с использованием оператора if # include < stdio.h > /* Пример 9 */ main() { int sgn; float x; printf (“ Введите число “); scanf (“%f”,&x); if ( x>0 ) { sgn=1; printf(“Число %f положительное sgn = %d\n”,x,sgn); } if ( x==0 ) { sgn=0; printf(“Число %f равно нулю sgn = %d\n”,x,sgn);} if ( x<0 ) { sgn=-1; printf(“Число %f отрицательное sgn =%d\n”,x,sgn); } } 27
Часто встречается необходимость использовать конструкцию if-else-if: if ( условие1 ) оператор1; else if ( условие2) оператор2; else if( условие3) оператор3; … else оператор; В такой форме условия оператора if проверяются сверху вниз. Как только некоторое условие принимает значение “ истинно”, выполняется оператор, следующий за этим условием, а вся остальная часть конструкции будет проигнорирована. Поэтому, предыдущую программу можно было бы написать следующим образом: # include < stdio.h > /* Пример 10 */ main() { int sgn; float x; printf(“ Введите число “); scanf(“%f”,&x); if ( x>0 ) { sgn=1; printf(“Число %f положительное sgn = %d\n”,x,sgn); } else if (x<0) {sgn=-1; printf(“Число %f отрицательное sgn = %d\n”,x,sgn); } else { sgn=0; printf(“Число %f равно нулю sgn = %d\n”,x,sgn); } } Для того чтобы проверить, равно число х нулю или не равно, можно написать if ( x==0 ) printf(“ Число равно нулю ”); else printf(“ Число не равно нулю ”); Такой же результат можно получить, написав if ( !x ) printf(“ Число равно нулю ”); else printf(“ Число не равно нулю ”); Вложенным оператором if называется конструкция вида if( x ) if( y ) оператор1; else оператор2; 28
В такой форме непонятно, к какому из операторов if относится else. В языке С оператор else ассоциируется с ближайшим if в соответствующем блоке. Поэтому в указанной конструкции else относится к if( y ). Для того чтобы отнести else к if( x), необходимо соответствующим образом расставить операторные скобки: if( x ){ if( y ) оператор1; } else оператор2; Теперь if( y ) относится к другому блоку. 3.2. Оператор множественного выбора Язык С имеет встроенный оператор множественного выбора, называемый switch. Основная форма оператора имеет вид /* Пример 11 */ switch ( выражение ) { case constant1: последовательность операторов break; switch ( выражение ) { case constant2: последовательность операторов break; … switch ( выражение ) { case constantN: последовательность операторов break; default последовательность операторов } Сначала вычисляется выражение в скобках за ключевым словом switch. Затем просматривается список меток ( case constant1 и т. д. ) до тех пор, пока не найдется метка, соответствующая значению вычисленного выражения. Далее происходит выполнение последовательности операторов, следующих за двоеточием. Если же значение выражения не соот29
ветствует ни одной из меток оператора switch, то выполняется последовательность операторов, следующих за ключевым словом default. Допускается конструкция оператора switch, когда слово default и соответствующая последовательность операторов может отсутствовать. Еще один не встречавшийся ранее оператор – break. Когда после последовательности операторов встречается ключевое слово break, то выполнение оператора break приводит к выходу из оператора switch и переходу к следующему оператору программы. Наличие оператора break в операторе switch необязательно. Оператор break заканчивает последовательность операторов, относящихся к каждой метке. Если же оператор break отсутствует, то выполнение switch продолжается до первого встретившегося break либо до конца самого оператора switch. Рассмотрим две программы с использованием оператора break и без него: # include < stdio.h > /* Пример 12 */ main() { char ch; printf(“ Введите заглавную букву русского алфавита ”); ch=getchar(); if ( ch >= ‘А’ && ch <=’Я’) switch (ch) { case ‘А’: printf(“Андреев\n”); break; case ‘Б’: printf(“Булгаков\n”); break; case ‘В’: printf(“Волошин\n”); break; case ‘Г’: printf(“Гоголь\n”); break; default: printf(“Достоевский, Зощенко и другие \n”); break; 30
} else printf(“ Надо было ввести заглавную букву\n”); } # include < stdio.h > /* Пример 13 */ main() { char ch; printf(“ Введите заглавную букву русского алфавита ”); ch=getchar(); if ( ch >= ‘А’ && ch <=’Я’) switch (ch) { case ‘А’: printf(“Андреев\n”); case ‘Б’: printf(“Булгаков\n”); case ‘В’: printf(“Волошин\n”); case ‘Г’: printf(“Гоголь\n”); default: printf(“Достоевский, Зощенко и другие \n”); } else printf(“ Надо было ввести заглавную букву\n”); } Предположим, вы запустили первую программу и ввели букву Б. Результатом выполнения программы будет следующая строка: Булгаков Выполняется только один оператор, соответствующий метке ‘Б’. В случае запуска следующей программы получим такой результат: Булгаков Волошин Гоголь Достоевский, Зощенко и другие Выполнились все операторы, начиная с метки ‘Б’, включая тот который следует за словом default. 31
3.3. Операторы циклов Циклы необходимы, когда необходимо повторить некоторые действия несколько раз, как правило, пока выполняется некоторое условие. В языке С имеются три оператора цикла: for, while и do-while. Основная форма цикла for имеет следующий вид: for ( инициализация; проверка условия; изменение) оператор; В более общем виде: for ( выражение1; выражение1; выражение1 ) оператор; В простейшей форме инициализация используется для присвоения начального значения параметру цикла. Проверка условия – обычно условное выражение, которое определяет, когда цикл должен быть завершен. Изменение обычно используется для изменения значения параметра цикла каждый раз при его повторении. Эти три раздела, определяющие заголовок цикла, должны разделяться точкой с запятой. Оператор (возможно составной) определяет тело цикла. Тело цикла выполняется до тех пор , пока проверка условия дает значение “ истинно”. Как только проверка условия даст значение “ ложно”, начинает выполняться следующий за оператором for оператор. Простой пример оператора for for ( i=0; i<10; i++) printf(“%d\n”,i); В результате выполнения этого оператора будут напечатаны в столбик цифры от 0 до 9. Для печати этих цифр в обратном порядке можно использовать следующий оператор: for ( i=9; i>=0; i—) printf(“%d\n”,i); Цикл for похож на аналогичные операторы цикла в других языках программирования, но в тоже время является более гибким и мощным по своим возможностям. В качестве параметра цикла можно использовать данные типа char, например: unsigned char ch; for ( ch=’A’; ch<=’Я’; ch++) printf(“%c”,ch); Следующий фрагмент программы for ( ch=’0’; ch!=N; ) scanf(“%c”,&ch); будет выполняться до тех пор, пока с клавиатуры не будет введен символ ‘N’. Заметим, что место в заголовке цикла, где должно быть изменение параметра цикла, пусто. При программировании, случайно или 32
намеренно может получиться цикл из которого нет выхода, так называемый бесконечный цикл. Приведем примеры: for ( ; ; ) printf(“ Бесконечный цикл ”); for ( i=1; 1; i++ ) printf(“ Бесконечный цикл ”); for ( j=12; j>8; j++ ) printf(“ Бесконечный цикл ”); Когда один цикл находится внутри другого, то говорят, что это вложенные циклы. Необходимость такой вложенности характерна для программирования многих задач, в частности, для заполнения различных таблиц. Рассмотрим программу печати таблицы умножения чисел от 1 до 9: # include < stdio.h > /* Пример 14 */ main() { int i,j; for ( i=1; i<10; i++); { for ( j=1; j<10; j++) printf(“%d * %d = %d”,i,j,i*j); printf(“\n”); } } Результатом выполнения этой программы будет построчная печать результатов умножения 1 на все числа от 1 до 9, на следующей строке – результаты умножения 2 на все числа от 1 до 9 и так далее. Необходимо помнить, что при использовании цикла for опасно изменять параметр цикла внутри тела цикла. Изменение значения параметра цикла внутри тела цикла может привести к мало понятному поведению программы. Следующий оператор цикла в языке С – это цикл while. Основная его форма имеет следующий вид: While ( условие ) оператор; оператор может быть простым, составным или пустым оператором (тело цикла while). Условие – это просто некоторое выражение. Цикл выполняется до тех пор, пока условие принимает значение “ истинно”. Когда условие принимает значение “ ложно”, программа передает управление следующему оператору программы. Также как и в цикле for, в цикле 33
while сначала проверяется условие, а затем выполняется оператор. Это так называемый цикл с предусловием. В цикле do-while условие проверяется в конце оператора цикла. Основная форма оператора do-while следующая: do { последовательность операторов } while ( условие ); Фигурные скобки необязательны, если внутри них находится один оператор. Тем не менее их лучше ставить для удобства чтения программы. Оператор do-while называется оператором цикла с постусловием. Какое бы условие ни стояло в конце оператора, набор операторов в фигурных скобках( тело цикла do-while ) один (первый) раз выполнится обязательно. В циклах for и while тело цикла может не выполниться ни разу. Рассмотрим пример программы с использованием цикла do-while. # include < stdio.h > # include < stdlib.h > # include < time.h > /* Пример 15 */ /* Угадываем число, заданное в диапазоне от 1 до 100 */ main() { int s,x; int n=0; randomize(); s=random(100)+1; /* генерация случайного числа */ do { printf(“ Введите число от 1 до 100:“); scanf(“%d”, &x); n++; if ( s<x ) printf(“ Заданное число меньше\n”); if ( s>x ) printf(“ Заданное число больше\n”); } while ( s-x ); printf(“ Вы угадали число \n”); printf(“ Затратили на угадывание %d попыток\n”,n); } В отличие от оператора for, в теле цикла операторов while и do-while (если оно не пустой оператор) обязательны действия, направленные на 34
изменения значения условия. Если таких действий нет, то циклы while и do-while могут стать бесконечными. В вышеуказанном примере это действия, связанные с изменением значения переменной x. 3.4. Операторы break и continue Оператор break имеет два применения. Первое – окончание case в операторе switch. Второе – немедленное окончание цикла, не связанное с проверкой обычного условия окончания цикла. Когда оператор break встречается внутри оператора цикла (является частью тела цикла), то происходит немедленный выход из цикла и переход к выполнению оператора, следующего за оператором цикла: # include < stdio.h > /* Пример 16 */ main() { int i; for ( i=0; i<1000; i++) { printf(“%d - %d\n”,i,i*i*i); if ( i*i*i > 10000 ) break } } В данном примере выход из цикла произойдет, когда значение куба переменной i превысит величину 10000. Еще один полезный оператор – continue. Если оператор continue встретился в теле цикла, то он передает управление на начало следующей итерации цикла. В циклах while и do-while на проверку условия, в цикле for – на изменение параметра цикла. Этот оператор необходим, когда надо закончить текущую итерацию цикла и не выполнять оставшиеся операторы, входящие в тело цикла # include < stdio.h > /* Пример 17 */ main() { int i; for ( i=7; i<1000; i++) { 35
if ( i%7 != 0 ) continue; printf(“%d”,i); } } При выполнении данной программы производится печать целых чисел, кратных 7 и не превышающих величину 1000. Если переменная i не кратна 7, осуществляется переход на следующую итерацию с новым значением переменной i. 3.5. Оператор безусловного перехода Оператор безусловного перехода имеет форму: goto метка; При написании программы можно вполне обходиться без этого оператора, но тем не менее он иногда бывает полезен. Метка – это идентификатор, за которым следует двоеточие. Меткой необходимо претворять оператор, на который надо сделать безусловный переход. Помеченный оператор должен находиться в той же функции, что и оператор goto. Наиболее полезен оператор goto для выхода из вложенных циклов, хотя в языке С имеются другие средства для решения этой задачи.
36
4. МАССИВЫ И УКАЗАТЕЛИ 4.1. Понятие массива, объявление массива На основе базовых (встроенных) типов данных язык С позволяет строить другие типы данных и структуры данных. Массив – одна из наиболее простых и часто используемых структур данных. Под массивом понимают набор данных одного и того же типа, собранных под одним именем. Каждый элемент массива определяется именем массива и порядковым номером элемента, который называется индексом. Индекс в языке С всегда целое число. Основная форма объявления массива размерности N такова: тип <имя массива>[размер1][размер2]…[размерN] Чаще всего используются одномерные массивы: тип <имя массива>[размер]; Тип – базовый тип элементов массива, размер – количество элементов одномерного массива. При описании двумерного массива его объявление имеет следующий вид: тип <имя массива>[размер1][размер2]; В этом описании можно трактовать объявление двумерного массива как объявление массива массивов, т. е. массив размера [размер2], элементами которого являются одномерные массивы <имя массива>[размер1]. Размер массива в языке С может задаваться константой или константным выражением. Нельзя задать массив переменного размера. Для этого существует отдельный механизм ( динамическое выделение памяти). Сначала обсудим более подробно одномерные массивы. В языке С индекс всегда начинается с нуля. Когда речь идет о первом элементе массива, то имеем в виду элемент с индексом 0. Если объявляется массив int a[100]; , то это значит, что массив содержит 100 элементов от a[0] до a[99]. Для одномерного массива легко определить, сколько байт в памяти будет занимать этот массив: 37
Количество байт=<размер базового типа>*<количество элементов> . В языке С под массив всегда выделяется непрерывное место в памяти. В языке С не проверяется выход значения индекса за пределы массива. Поэтому, при написании и отладке программ с использованием массивов, этот момент необходимо учитывать. 4.2. Массивы символов, строки. Функции работы со строками Массивы типа char – символьные массивы – занимают особое место. Во многих языках программирования есть специальный тип данных – строка символов (string). В языке С такого типа нет, а работа со строками реализована путем использования одномерных массивов типа char. В языке С символьная строка – это одномерный массив типа char, заканчивающийся нулевым байтом. Для нулевого байта определена специальная символьная константа – ‘\0’. Это необходимо учитывать при описании соответствующего символьного массива. Так, если строка должна содержать N символов, то в описании массива необходимо указать N+1 элемент. Например, описание int str[12], предполагает, что строка содержит 11 символов, а последний байт зарезервирован под нулевой байт. Хотя в языке С нет специального типа строки, язык допускает строковые константы. Строковая константа – это список литер, заключенных в двойные кавычки. Например, “Borland C++”, “Строковая константа”. В конец строковой константы не надо ставить символ ‘\0’. Это сделает сам компилятор, и строка “Borland C++” в памяти будет выглядеть так: B
o
r
l
a
n
d
C
+
+
\0
Есть два простых способа ввести строку с клавиатуры. Первый способ – воспользоваться функцией scanf() со спецификатором ввода %s. Надо помнить , что функция scanf() вводит символы до первого пробельного символа. Второй способ – воспользоваться функцией gets(), объявленной в файле stdio.h. Функция gets() позволяет вводить строки, содержащие пробелы. Ввод заканчивается нажатием клавиши Enter. Обе функции автоматически ставят в конце строки нулевой байт. В качестве параметра в этих функциях используется имя массива. 38
Вывод строк производится с помощью функций printf() и puts(). Обе функции выводят содержимое массива до первого нулевого байта. Функция puts() добавляет в конце выводимой строки символ новой строки. В функции printf() переход на новую строку надо предусматривать в строке формата самим. Рассмотрим пример: # include < stdio.h > /* Пример 18 */ /* Ввод строки с клавиатуры и вывод ее на экран */ main() { char str[80]; printf(“Введите строку длиной не более 80 символов”); gets(str); printf(“ Вы ввели строку %s\n”,str); printf(“Введите еще одну строку ”); scanf(“%s”,str); printf(“ Вы ввели строку ”); puts(str); } Для работы со строками существует специальная библиотека, описание которой находится в файле string.h. Рассмотрим их подробнее. Вызов функции strcpy() имеет вид – strcpy(s1,s2). Эта функция используется для копирования содержимого строки s2 в строку s1. Массив s1 должен быть достаточно большим, чтобы в него поместилась s2. Если места мало, то компилятор не выдаст соответствующего сообщения об ошибке, также это не приведет к прерыванию выполняемой программы, но может привести к порче других данных, что отразится на результате. Вызов функции strcat() имеет вид – strcat(s1,s2). Эта функция присоединяет строку s2 к строке s1 и помещает ее в массив, где находится строка s1, при этом строка s2 не изменяется. Нулевой байт, который завершал строку s1, будет заменен первым символом строки s2. И в функции strcpy(), и в функции strcat() полученная строка автоматически завершается нулевым байтом. Рассмотрим пример использования этих функций: 39
# include < stdio.h > # include < string.h > /* Пример 19 */ main() { char s1[20], s2[20]; strcpy(s1,” Hello, “); strcpy(s2,” World !”); puts(s1); puts(s2); strcat(s1,s2); puts(s1); puts(s2); } Вызов функции strcmp() имеет вид – strcmp(s1,s2). Эта функция сравнивает строки s1 и s2 и возвращает значение ноль, если строки равны, т. е. содержат одно и то же число одинаковых символов. Под сравнением строк понимается сравнение в лексикографическом смысле. Если s1 лексикографически больше s2, то функция strcmp() возвращает положительное значение, если меньше – отрицательное. Вызов функции strlen() возвращает длину строки s, при этом завершающий нулевой байт не учитывается. Вызов strlen(“hello”) вернет значение 5. Рассмотрим пример программы, в которой определяется длина строки, вводимой с клавиатуры: # include < stdio.h > # include < string.h > /* Пример 20 */ main() { char s[80]; printf(“ Введите строку: ”); gets(s); printf(“строка \n%s\n имеет длину %d символов\n”,s,strlen(s)); }
40
4.3. Двумерные массивы Язык С допускает многомерные массивы, простейшими из которых являются двумерные массивы. Можно сказать, что двумерный массив – это массив одномерных массивов. Двумерный массив int a[3][4] можно представить в виде табл. 4.1. Таблица 4.1 Двумерный массив int a[3][4]
a[0][0] a[1][0] a[2][0]
a[0][1] a[1][1] a[2][1]
a[0][2] a[1][2] a[2][2]
a[0][3] a[1][3] a[2][3]
Здесь первый индекс – номер строки, второй номер столбца. Количество байт памяти, необходимое для хранения двумерного массива в памяти определяется как <размер типа данных>*<число строк>*<число столбцов>. В памяти компьютера двумерный массив располагается по строкам. Память для массивов, которые определены как глобальные, отводится в процессе компиляции и сохраняется на все время выполнения программы. Часто двумерные массивы используются для работы с числовыми и с символьными таблицами(массивы строк). Рассмотрим пример: # include < stdio.h > # include < string.h > /* Пример 21 */ main() { char text[5][20]; strcpy(text[0],”Turbo Basic”); strcpy(text[1],”Turbo Pascal”); strcpy(text[2],”Borland C++”); strcpy(text[3],”Turbo Prolog”); strcpy(text[4],”Turbo Fortran”); } В данной прграмме заполняется массив text[][], причем, в функции strcpy при заполнении массива, используется только первый индекс. Заполнение text[][] иллюстрируется табл. 4.2. 41
Таблица 4.2 Расположение массива text[][] в памяти компьютера
T T B T T
u u o u u
r r r r r
b b l b b
o o a o o
B a s i c \0 P a s c a l \0 n d C + + \0 P r o l o g \0 F o r t r a n \0
4.4. Инициализация массивов При программировании бывает важным уметь инициализировать массивы, т. е. присваивать элементам массива некоторые начальные значения. Самый простой способ инициализации – указать список инициализаторов в фигурных скобках при объявлении массива, например: float ff[5]={1.4, 2.5, 3.6, 12.8, 0.9}; int z[3][4]={1,2,3,4,5,6,7,8,9,10,11,12}; Многомерные массивы можно инициализировать, рассматривая их как массивы массивов: char str z[3][4]={ {1,2,3,4},{5,6,7,8},{9,10,11,12}}; Количество инициализаторов может быть меньше, чем количество элементов массива. В этом случае оставшиеся элементы массива считаются неопределенными. Символьные массивы могут инициализироваться как обычный массив: char str[12]={‘a’,’b’,’c’,’d’}; а могут – как строка символов: char str[12]=“abcd”; Отличие состоит в том, что во втором случае будет добавлен автоматически нулевой байт. Допускается объявление и инициализация массива без явного указания его размера: char str[]=”abcd”; В этом случае компилятор сам определит необходимое количество элементов массива, включая нулевой байт. Таким образом можно объявлять массивы любого типа: int a[]={10, 20, 30, 40, 50}; 42
Но при объявлении многомерных массивов с неизвестным количеством элементов, можно не указывать размер только в самых левых квадратных скобках: int a[][3]={1, 2, 3 5, 6, 7 8, 9, 10}; Рассмотрим пример использования массивов на примере задачи сортировки одномерного целочисленного массива по убыванию значений: # include < stdio.h > /* Пример 22 */ main() { int arr[10]={9, 12, 43, 2, 4, 78, 15, 34, 11, 27}; int i,j,tmp; printf(“ Неотсортированный массив: ”); for ( i=0; i<10; i++) printf(“%d “, arr[i]); printf(“\n”); for ( i=0; i<8; i++) for ( j=i+1; j<9; j++) if( arr[i] < arr[j] ) { tmp=arr[i]; arr[i]=arr[j]; arr[j]=tmp; } printf(“ Отсортированный массив: ”); for ( i=0; i<10; i++) printf(“%d “, arr[i]); printf(“\n”); } 4.5. Указатели, объявление указателей, операции над указателями Понимание и правильное использование указателей является основой для создания профессиональных программ на языке С. Указатель – это переменная, которая предназначена для хранения и использования в программе адреса некоторого объекта. Здесь имеется в 43
виду адрес в памяти компьютера. Адрес представляет собой простое целое число, но его нельзя трактовать как переменную или константу целого типа. Если переменная по смыслу является указателем, то она должна быть соответствующим образом объявлена. Форма объявления указателя следующая: Тип *<имя переменной>; В этом объявлении тип – некоторый допустимый для языка С тип данных, на который указывает указатель. Знак * – означает, что следующая за ним переменная является указателем. Например: char *ch; int *temp, i, *j; float *pf, f; Здесь объявлены указатели ch,temp,j,pf, переменная i типа int и переменная f типа float. С указателями связаны две специальные операции & и *. Обе эти операции являются унарными, т. е. имеют один операнд, перед которым они ставятся. Операция & соответствует по смыслу операции взятия (определения) адреса. Операция * является по смыслу операцией взятия (определения) значения по указанному адресу. Данные операции нельзя спутать с соответсвующими им по написанию бинарными операциям – поразрядным AND и операцией умножения, так как они являются унарными, что всегда видно из контекста программы. При объявлении указателя очень важным является базовый тип, который сообщает компилятору сколько байт памяти занимает переменная, на которую указывает данный указатель. Простейшие действия с указателями проиллюстрируем на примере следующей программы: # include < stdio.h > /* Пример 23 */ main { float x=12.3, y; float *p; p=&x; y=*p; printf(“ x = %f y = %f”,x,y); 44
*p++; /* Увеличиваем на 1 значение, взятое по указателю p */ printf(“ x = %f y = %f”,x,y); y=1+*p*y; /* Добавляем 1 к произведению значения взятого по указателю p на y */ printf(“ x = %f y = %f”,x,y); } К указателям можно применять операцию присваивания, если они являются указателями одного типа. Например: # include < stdio.h > /* Пример 24 */ main { int x=12; int p, g; p=&x; g=p; printf(“%p”,p); printf(“%p”,g); printf(“%d %d”,x,*g); } В этом примере приведена спецификация формата %p функции printf(), которая используется для вывода адреса памяти в шестнадцатеричной форме. Нельзя создать переменную типа void, но можно создать указатель на такой тип. Указателю на void можно присвоить указатель любого другого типа. При обратном присваивании необходимо использовать явное преобразование указателя на void. Например, рассмотрим следующий фрагмент: void *pv; float f, *pf; pf=&f; pv=pf; pf=(float*)pv; В языке С допустимо присвоить указателю любой адрес памяти. Однако если объявлен указатель на целое (int p), а по адресу, который присвоен данному указателю, находится переменная типа float (float x), 45
то при компиляции программы будет выдано сообщение об ошибке в строке p=&x . Эту ошибку можно исправить, преобразовав указатель на int к типу указателя на float явным преобразованием типа: p=(int*)&x; , но при этом теряется информация о том, на какой тип указывал исходный указатель. Над указателями можно производить арифметические операции: сложение и вычитание. Арифметические действия над указателями имеют свои особенности. Рассмотрим программу: # include < stdio.h > /* Пример 25 */ main { int *p; int x=12; p=&x; printf(“%p %p”,p,++p); } При выполнении этой программы увидим, что при операции ++p, значение указателя p увеличиться на 2, а не на 1. Это правильно, так как следующее значение указателя указывает на адрес следующего целого, а не на следующий адрес (целое занимает 2 байта). Таким образом, при каждой операции ++p значение указателя будет увеличиваться на количество байт, занимаемой переменной базового типа указателя. К указателям можно прибавлять или вычитать некоторое целое. Пусть указатель p имеет значение 4000 и указывает на целое. Тогда в результате выполнения оператора p=p+5; значение указателя станет равным 4010. Общая формула для вычисления значения указателя после выполнения операции p=p+m; будет иметь вид: =
+m*<количество байт базового типа указателя> Аналогичны правила вычитания целых констант от значения указателя. Кроме того, можно вычитать один указатель из другого. Так, если p1 и p2 – указатели на элементы одного и того же массива, то операция p1-p2 дает такой же результат, как вычитание индексов соответствующих элементов массива. Другие арифметические операции над указателями запрещены, т. е. нельзя складывать два указателя, умножать друг на друга и делить и т. д. 46
Указатели можно сравнивать. Применимы все 6 операций сравнения. Сравнение p # include < ctype.h > /* Пример 26 */ main() { char str[]=”String From Letters in Different Registers”; int i; printf(“Строка Будет Напечатана Заглавными Буквами”); while ( str[i] ) printf(“%c”, toupper(str[i++])); } # include < stdio.h > # include < ctype.h > main() { char str[]=”String From Letters in Different Registers”; 47
char *p; p=str; printf(“Строка будет напечатана строчными буквами”); while ( str[i] ) printf(“%c”, tolower(*p++)); } Если в этих примерах заменить строку на английском языке на строку, набранную русскими буквами, то никакого преобразования букв в строчные, или наоборот, в прописные не произойдет. Это связано с тем, что стандартные функции toupper() и tolower() анализируют значения аргумента и возвращают то же самое значение, если он не является соответственно строчной или прописной буквой латинского алфавита. Если же аргумент является строчной буквой латинского алфавита, то значением функции toupper() будет соответствующая прописная буква (точнее, код этой буквы). Функция tolower() изменяет код только прописных букв латинского алфавита. Прототипы этих функций находятся в файле ctype.h. 4.7. Массивы указателей Указатели, как и переменные любого другого типа, могут объединяться в массивы. Объявление массива указателей на 12 чисел имеет вид int *x[11]; . Каждому из элементов массива можно присвоить адрес; например, пятому элементу этого массива присвоим адрес целой и ранее объявленной переменной y: x[4]=&y;. Если затем необходимо найти значение переменной y, то это можно сделать, выполнив операцию *x[4]. Рассмотрим пример использования массива указателей: # include < stdio.h > # include < string.h > # include < stlib.h > # include < conio.h > /* Пример 27 */ main() { char *ext[]={“exe”,”com”,”dat”,”c”,”pas”,”cpp”}; char ch, sl[80]; for( ; ; ) { do 48
{ printf(“ Файлы с расширением:\n”); printf(“1. exe\n”); printf(“2. com\n”); printf(“3. dat\n”); printf(“4. c\n”); printf(“5. pas\n”); printf(“6. cpp\n”); printf(“7. Exit\n”); printf(“ Ваш выбор: \n”); ch=getche(); printf(“\n”); } while ((ch<’1’)||(ch>’7’)); if ( ch == 7 ) break; strcpy(sl,”dir *.”); strcat(sl,ext[ch-49]; system(sl); } } Данная программа формирует командную строку с учетом пожеланий пользователя и затем выполняет ее, используя библиотечную функцию system(). Данная функция выполняет указанную в командной строке команду (dir – вывод справочника файлов). Расширение для имен выводимых файлов задается пользователем. Константа 49 условно состоит из двух слагаемых – 48 и 1. 48- это символьный код нуля. Поэтому, если пользователь программы хочет вывести файлы с расширением pas , он указывает символьную пятерку, которой соответствует код – 53. Разность 53-49 определяет индекс пятого элемента в массиве ext (первый элемент массива в С имеет нулевой индекс). Часто массив указателей используется, если надо иметь ссылки на стандартный набор строк. Например, для хранения сообщений о возможных ошибках удобно сделать так: char *errors[]={“Cannot open file”, “Cannot close file”, “Allocation error”, “System error” }; 49
При таком объявлении строчные константы будут занесены в раздел констант в памяти, массив указателей будет состоять из четырех элементов, под которые будет выделена память, и эти элементы будут инициированы адресами, указывающими на начало строчных констант. Строчная константа в языке С ассоциируется с адресом начала строки в памяти, тип строки получает char*(указатель на тип char). Поэтому возможно и активно используется следующее присваивание: char *p; p=”Hello, World !”; В языке С возможна ситуация, когда указатель указывает на указатель. В этом случае описание имеет следующий вид: int **point; Здесь point имеет тип указатель на указатель int. Соответственно, чтобы получить целочисленное значение переменной, на которую указывает point, надо в выражении использовать **point. Рассмотрим пример: # include < stdio.h > /* Пример 28 */ main() { int i; int *pi; int **ppi; i=12; pi=&i; ppi=π printf(“i = %d pi = %p ppi = %p **ppi = %d\n”,i,pi,ppi,**ppi); } После того, как указатель был объявлен, но до того, как ему было присвоено значение, указатель содержит неопределенное значение. Попытка использовать такое значение может вызвать ошибку при выполнении программы, и даже, нарушить работу операционной системы. Принято считать, что указатель с неопределенным значением должен иметь значение null, но это не делает такой указатель “безопасным”. С другой стороны нулевой указатель иногда удобно использовать как признак окончания некоторого массива указателей или списка, поэтому он часто фигурирует в операциях сравнения. 50
5. ФУНКЦИИ 5.1. Объявление функций. Оператор return Функция – это некоторая логически законченная совокупность операторов языка, которая выполняет определенную конкретную задачу, и может быть вызвана для своего выполнения необходимое количество раз. Основная форма описания функции имеет вид: Тип < имя функции > ( список параметров ) { тело функции } Тип определяет тип значения, которое возвращает функция с помощью оператора return. Если тип не указан, то по умолчанию предполагается, что функция возвращает целое значение ( типа int ). Список параметров состоит из перечня типов и имен параметров, разделенных запятыми. Функция может не иметь параметров, но круглые скобки в ее описании необходимы в любом случае. В списке параметров для каждого параметра должен быть указан тип. Оператор return имеет два варианта использования. Во-первых, этот оператор вызывает немедленный выход из текущей функции и возврат в вызывающую программу. Вовторых, этот оператор может использоваться для возврата значения функции. В теле функции может быть несколько операторов return, но может быть ни одного. В этом случае возврат в вызывающую программу происходит после выполнения последнего оператора в теле функции. Приведем пример функции, реализующей возведение числа a в натуральную степень b : /* Пример 29 */ float step(float a, int b) { float i; 51
if ( a < 0 ) return (-1); for ( i=1; i<=b; i++) a*=a; return a; } Эта функция возвращает значение –1, если основание отрицательное, и a в степени b , если основание неотрицательное. Другой пример – функция для нахождения наибольшего из двух целых чисел: /* Пример 30 */ int max( int a, int b) { int m; if ( a > b ) m=a; else m=b; return m; } Можно написать эту функцию, не используя дополнительную переменную: /* Пример 31 */ int max( int a, int b) { if ( a > b ) return a; else return b; } Еще короче: int max( int a, int b) { if ( a > b ) return a; return b; } В случае, когда оператор return отсутствует в теле функции, или за ним нет значения, то возвращаемое значение не определено. Если функция должна возвращать значение в соответствии со своим описанием, но не делает этого, компилятор выдаст предупреждение. Все функции, возвращающие значения, могут использоваться в выражениях языка С, но они не могут использоваться в левой части оператора присваива52
ния, за исключением тех случаев, когда возвращаемым значением является указатель. Использование функций, возвращающих указатели, имеет некоторые особенности, так как их значениями являются адреса памяти данных определенного типа. Рассмотрим пример функции, возвращающей указатель на тип char. Эта функция находит в строке первый пробел и возвращает его адрес: /* Пример 32 */ char* find( char* string ) { int i=0; while (( string[i] != ‘ ‘) && (string[i] != ‘\0’)) i++; if ( string[i] == ‘ ‘ ) return &string[i]; else return NULL } Когда функция не должна возвращать никакого значения, она имеет тип void. Например, для вывода на экран горизонтальной строчки, состоящей из заданного символа, начиная с текущего положения курсора, можно использовать следующую функцию: /* Пример 33 */ void gorisont_line( char ch ) { int i; for ( i=0; i<80; i++ ) printf(“%c”,ch); } Если для функции, которая не возвращает значение, не объявить тип void, то она по умолчанию будет иметь тип int, что вызовет предупреждающее сообщение компилятора. Объявление типа возвращаемого значения функции, даже если в этом нет необходимости, является хорошим неформальным правилом при программировании. 5.2. Прототипы функций Особенностью стандарта ANSI языка С является то, что для создания правильного машинного кода функции ему необходимо сообщить до ее первого вызова тип возвращаемого результата, а также количество и 53
типы аргументов. Для этого в языке С используется понятие прототипа функции. Прототип функции задается следующим образом: Тип < имя функции> ( список параметров ); Использование прототипа функцией является ее объявлением. Чаще всего прототип функции полностью совпадает с заголовком в описании функции, хотя это и не всегда так. При объявлении функции компилятору важно знать имя функции, количество и тип параметров и тип возвращаемого значения. При этом имена формальных параметров не играют никакого значения и игнорируются компилятором. Поэтому прототип функции может выглядить как: /* Пример 34 */ int func( int a, float b, char* c); или так int func( int , float , char* ); Рассмотрим пример: # include < stdio.h > float sqr( float a ); main() { float b=7.8; printf(“ Квадрат числа %f равен %f”,b,sqr(b)); } float sqr(float a) { return a*a } Если функция не имеет аргументов, то при объявлении прототипа такой функции следует вместо аргументов писать ключевое слово void. Использование данного ключевого слова необходимо и при объявлении функции main(). Такое объявление может иметь вид void main(void) или main(void). 5.3. Область действия и область видимости Область действия переменной – это правила, которые устанавливают, какие данные доступны из данного места программы. В языке С каждая функция – это отдельный блок программы. Попасть в тело функции нельзя иначе, как через вызов данной функции. 54
В частности, нельзя оператором локального перехода goto перейти в середину другой функции. С точки зрения области действия переменных различают три типа переменных: глобальные, локальные и формальные параметры. Правила области действия определяют, где каждая из них может применяться. Локальные переменные – это переменные, объявленные внутри блока, в частности внутри функции. Язык С поддерживает простое правило: переменная может быть объявлена внутри любого блока программы. Локальная переменная доступна внутри блока, в котором она объявлена. Вспомним, что блок открывается фигурной скобкой и закрывается фигурной скобкой. Область действия локальной переменной – блок. Локальная переменная существует пока выполняется блок, в котором эта переменная объявлена. При выходе из блока эта переменная (и ее значение) теряется # include < stdio.h > void ff( void ); /* Пример 35 */ main ( void ) { int i=1; ff(); printf(“ В функции main значение i равно %c\n”,i); } void ff(void) { int i=12; printf(“ В функции ff значение i равно %c\n”,i); } Пример показывает, что при вызове функции значение переменной i, объявленной в main(), не изменилось. Формальные параметры – это переменные, объявленные при описании функции как ее аргументы. Функции могут иметь некоторое количество параметров, которые используются при вызове функций для передачи значений в тело функции. Формальные параметры могут использоваться в теле функции так же, как локальные переменные, которыми они по сути дела и являются. Область действия формальных параметров – блок, являющийся телом функции. 55
Глобальные переменные – это переменные, объявленные вне какойлибо функции. В отличие от локальных переменных глобальные переменные могут быть использованы в любом месте программы, но перед их первым использованием они должны быть объявлены. Область действия глобальной переменной – вся программа. Использование глобальных переменных имеет свои недостатки: – они занимают память в течение всего времени работы программы; – использование глобальных переменных делает функции менее общими и затрудняет их использование в других программах; – использование внешних переменных делает возможным появление ошибок из-за побочных явлений. Эти ошибки, как правило, трудно отыскать. 5.4. Классы памяти В языке С имеется инструмент, позволяющий управлять использованием памяти и , тем самым, создавать гибкие программы. Этот инструмент – классы памяти. Каждая объявленная переменная принадлежит к одному из четырех классов памяти, которые описываются следующими ключевыми словами: auto – автоматическая; extern – внешняя; static – статическая; register – регистровая. Тип памяти указывается модификатором – ключевым словом, стоящим перед указанием типа переменной. Например, static int sum; register int plus. Если ключевое слово перед спецификацией типа локальной переменной при ее объявлении нет, то по умолчанию она принадлежит классу auto. Поэтому практически это слово не используется. Автоматические переменные (auto) имеют локальную область действия. Они известны только внутри блока, в котором определены. Другие функции могут использовать то же имя, но это должны быть переменные, относящиеся к разным блокам. Автоматическая переменная создается (под нее отводится память) при входе в блок функции. При выходе из блока автоматическая переменная пропадает, а область памя56
ти, в которой находилась эта переменная, считается свободной и может использоваться для других целей. Автоматические переменные хранятся в оперативной памяти, точнее в той области памяти, которая отводится под стек. Регистровые (registr) переменные хранятся в регистрах процессора, доступ к которым значительно быстрее, чем к автоматическим переменным. В остальном регистровые переменные аналогичны автоматическим переменным. Регистровая память процессора невелика, и если доступных регистров нет, то переменная становится простой автоматической переменной. Описание регистровой переменной имеет вид: registr int q; Внешняя переменная (extern) относится к глобальным переменным. Она может быть объявлена как вне , так и внутри тела функции: void ff(void) { …………… extern int x; /* объявление переменной внутри функции*/ …………… } Появление ключевого слова extern связано с модульностью языка С, т. е. возможностью составлять многофайловую программу, где каждый файл компилируется отдельно. Когда мы в одном из файлов опишем вне тела функции глобальную переменную, например так – float global; , то для нее выделится место в памяти в разделе глобальных переменных и констант. Если мы используем эту глобальную переменную в другом файле, то при раздельной компиляции компилятор не будет знать, что это за переменная. Использование объявления extern float global; не приводит к выделению памяти, а сообщает компилятору, что такая переменная будет описана в другом файле. И тогда при компоновке программы, состоящей из нескольких файлов, компоновщик будет искать описание этой переменной и связывать ее с использованием в других файлах. Объявление внешней переменной может быть как вне функции, так и внутри функции. Если это же имя без ключевого слова extern будет объявлено внутри функции, то под этим именем будет создана уже другая автоматическая переменная. Можно к объявлению этой переменной добавить ключевое слово auto, чтобы показать, что вы не ошиблись, а намеренно продублировали имя. Объявлений переменной как внешней может быть несколько, в том числе и в одной функции 57
или в одном файле. Описание же переменной должно быть только одно. Следующие примеры демонстрируют разные способы описания внешних переменных: /* Пример 36 */ int var; /* описана внешняя переменная var */ main(void) { extern int var; /* объявлена та же внешняя переменная */ ………… } func1() { extern int var1; /* объявлена внешняя переменная var1 */ ………… /* переменная var также внешняя*/ } func2() /* переменная var внешняя */ { /* переменная var1 невидима для этой функции */ ………… } int var1; /* описание внешней переменной */ func3() /* для этой функции var1 - внешняя */ { int var; /* здесь var – локальная и не связана*/ ………… /* с соответствующей глобальной */ } func4() /* здесь var является внешней и глобальной */ { auto int var1; /* var1- локальная и автоматическая */ ………… } При описании статических переменных перед указанием типа ставится ключевое слово static. Область действия локальной статической переменной является вся программа. Место в памяти под локальные статические переменные выделяется в начале работы программы в разделе глобальных и статических переменных. Однако область видимости локальных статических переменных такая же, как и 58
у автоматических. Значение статических переменных сохраняется от одного вызова функции до другого. Локальные статические переменные инициализируются нулем, если не указан другой инициализатор. При этом описание с инициализацией static int count=12; вызывает однократную инициализацию переменной count при выделении для нее места. При последующих вызовах функции, в которой описана эта переменная, инициализации не происходит. Это позволяет использовать такую переменную для организации счетчика количества вызовов функции. # include < stdio.h > void trystat(void); /* Пример 37 */ main(void) { int i; for ( i=1; i<=3; i++) { printf(“ вызов - %d\n”,i); trystat(); printf(“ вызов - %d\n”,i); trystat(); } } void trystat(void) { int aut_l=1; static int stat_l=1; printf(“aut_l = %d stat_l = %d\n”,aut_l++,stat_l++); } Можно описать также глобальную статическую переменную, т. е. описать переменную типа static вне любой функции. Отличие внешней переменной от внешней статической переменной состоит в том, что внешняя переменная может использоваться функциями в любом файле, а внешняя статическая переменная может использоваться только функциями того файла, где она описана, причем только после ее определения. Все глобальные переменные, как статические так и нестатические, инициализируются нулем, если в программе не предусмотрено другой 59
инициализации. В табл. 5.1 приведены область действия и продолжительность существования переменных разных классов памяти: Таблица 5.1 Область действия и продолжительность существования переменных разных классов памяти Ключевое слово
Время существования
Область действия
Автоматический Регистровый Статический локальный Статический глобальный
auto registr static static
Временно Временно Постоянно Постоянно
Блок Блок Блок Ф ай л
Внешний
extern
Постоянно
Программа
Класс памяти
В программе может быть описано несколько переменных с одним и тем же именем, но в разных блоках. В нижеприведенном примере объявлена одна глобальная и три локальных переменных с одним и тем же именем var: # include < stdio.h > void ff( void ); void fl( void ); /* Пример 38 */ int var=5; main( void ) { int var=10; printf(“ var = %d\n”,var); /* var = 10 */ { int var=100; printf(“ var = %d\n”,var); /* var = 100 */ } printf(“ var = %d\n”,++var); /* var = 11 */ ff(); printf(“ var = %d\n”,++var); /* var = 12*/ fl(); printf(“ var = %d\n”,++var); /* var = 13 */ fl(); 60
printf(“ var = %d\n”,++var); /* var = 14 */ } void ff( void ) { int var=55; printf(“ var = %d\n”,var); /* var = 55 */ } void fl( void ) { printf(“ var = %d\n”,var); /* var = 5 */ } 5.5. Параметры и аргументы функций В языке С все аргументы функции передаются по значению. При вызове функции в стеке выделяется место для формальных параметров функции, и в это место заносится значение фактического параметра, т. е. значение параметра при вызове функции. Далее функция использует это значение, при этом она может изменить значение параметра. При выходе из функции измененные значения параметров теряются. Таким образом, в языке С вызванная функция не может изменить значения переменных, указанных в качестве фактических параметров при обращении к ней. Например, функция swap(), которая должна менять значение параметров местами, не будет этого делать: /* Пример 39 */ void swap(int a, int b) { int tmp=a; a=b; b=tmp; } Но тем не менее функцию можно приспособить для изменения аргументов. Для этого необходимо в качестве параметра передавать адрес переменной, которую надо изменять, т. е. передавать указатель на переменную. Такой прием в языке С называется передачей параметра по ссылке. Вызванная функция должна описывать аргумент как ссылку и обращаться к фактической переменной косвенно, через эту ссылку. Если 61
в качестве аргумента берется имя массива, то передаваемое функции значение фактически есть адрес первого элемента массива. Теперь функцию swap() можно описать следующим образом: /* Пример 40 */ void swap(int *a, int *b) { int tmp=*a; *a = *b; *b=tmp; } Для иллюстрации использования этих двух способов передачи параметров приводим текст следующей программы: # include < stdio.h > /* Пример 41 */ void swap( int a, int b); void swap1( int *a, int *b); void main( void ) { int x=10, y=20; printf(“ Сначала x = %d y = %d\n”,x,y); swap(x,y); printf(“ Теперь x = %d y = %d\n”,x,y); printf(“ Ничего не изменилось \n”); swap1(&x,&y); printf(“ Теперь x = %d y = %d\n”,x,y); printf(“ Значения поменялись \n“); } void swap(int a, int b) { int tmp=a; a=b; b=tmp; } void swap1(int *a, int *b) { int tmp=*a; 62
*a = *b; *b=tmp; } Результатом работы этой программы будет следующее: сначала x=10 y=20, потом x=20 y=10. Значения переменных x и y изменились, так как были переданы по ссылке. Еще одна особенность состоит в том, что при вызове функции swap() можно задать конкретные значения, например swap(10,20). Вызвать функцию swap1() в виде swap1(&10,&20) нельзя. Если в качестве аргумента функции используется массив, есть лишь один способ – передача параметра по ссылке. Сделать это можно тремя способами: func(int ar[10]); func(int ar[]); func(int *ar); Все три способа дадут один и тот же результат. Рассмотрим программу, где используется функция сортировки массива по возрастанию значений: # include < stdio.h > /* Пример 42 */ void sort(int arr[], int n); void main( void ) { int mass[10]={1,-6,21,3,-7,4,-12,9,5,17}; int i, size=10; printf(“ до сортировки: \n“); for ( i=0; i<10; i++) printf(“%d”,mass[i]); printf(“\n”); sort(mass,size); printf(“ после сортировки: \n“); for ( i=0; i<10; i++) printf(“%d”,mass[i]); } void sort(int arr[], int n); { 63
int i,j,tmp; for ( i=0; i
5.7. Указатель на функцию На функцию, как и на другие объекты языка С можно создать указатель. Указатель pfunc на функцию, на функцию возвращающую значение типа type и имеющую параметры типа type1, type2, объявляется следующим образом: type (*pfunc)(type1 t1,type2 t2); По определению указатель на функцию содержит адрес первого байта или слова выполняемого кода функции. Над указателями на функцию запрещены арифметические операции. Использование указателя на функцию имеет несколько применений, в частности он используется, если необходимо передать функцию как параметр другой функции. Рассмотрим пример использования указателя на функцию: # include < stdio.h > # include < math.h > /* Пример 44 */ double f(double x); double f1(double x); double f2(double x); double ff(double (*pf)(double x), double x); /* указатель на функцию в качестве аргумента*/ void main(void) { double z=2.3, y; double (*ptrf)(double x); /* указатель на функцию */ ptrf=f; /* инициализация указателя */ y=(*ptrf)(z); /* вызов функции через указатель – 1-й способ*/ printf(“z=%f y=%f\n”,z,y); y=ptrf(z); /* вызов функции через указатель – 2-й способ*/ printf(“z=%f y=%f\n”,z,y); ptrf=sin; /* использование стандартной функции*/ y=ptrf(z); printf(“z=%f sin(z)=%f\n”,z,y); y=(*ptrf)(4.6) /* можно таким образом */ printf(“z=%f y=%f\n”,z,y); y=ff(f1,z); /* вызов ff с параметром f1*/ printf(“z=%f y=%f\n”,z,y); 65
y=ff(cos,z); /* другой вызов с использованием стандартной функции*/ printf(“z=%f y=%f\n”,z,y); y=ff(ptrf,z); /* то же через указатель на функцию*/ printf(“z=%f y=%f\n”,z,y); } double f(double x) { puts(“ In f()”); return x; } double f1(double x) { puts(“ In f1()”); return x*x; } double f2(double x) { puts(“ In f2()”); return x*x*x; } double ff(double (*pf)(double x), double x) /* Функция с указателем на функцию в качестве параметра*/ { puts(“ In ff() %f\n”,pf(x)); return pf(x); } При использовании указателей на функции можно образовывать массивы указателей. Такая структура данных называется jump table. Если определены объявления int f0(void); int f1(void); int f2(void); int f3(void); int(*jtable[](void)=(f0,f1,f2,f4); Соответствующую функцию можно вызвать val = (*jtable[i])(); или val = jtable[i](); 66
6. ТИПЫ ДАННЫХ, ОПРЕДЕЛЯЕМЫЕ ПОЛЬЗОВАТЕЛЕМ 6.1. Структура Пользователь при написании программы на языке С может создать пять типов данных: – структуры (structure), – объединения (union), – перечисляемый тип (enumeration), – поля битов (bit fields), – с помощью оператора typedef создать новое имя (псевдоним) для уже существующего типа. Структура объединяет несколько переменных, возможно разного типа. Переменные, которые объединены структурой, называются членами, элементами или полями структуры. Пример определения структуры: /* Пример 45 */ struct student{ char name[30]; int kurs; char group[3]; int stip; }; Объявление структуры является оператором, и поэтому после такого объявления должна состоять точка с запятой. При этом надо понимать, что пока никакая переменная не объявлена, так как выделения памяти под переменную не произошло. Здесь под именем student задан вид структуры, иначе говоря, ее шаблон, и определен новый тип struct student. Для того чтобы объявить конкретные переменные типа struct student, можно записать struct student stud1, stud2; Теперь объявлены две переменные – stud1 и stud2. Компилятор автоматически выделит под них место в памяти компьютора. Под каждую 67
из переменных типа структуры выделяется непрерывный участок памяти. Задание шаблона структуры и объявление переменных может производится и в одном операторе /* Пример 46 */ struct student{ char name[30]; int kurs; char group[3]; int stip; } stud1, stud2; Здесь одновременно задается структура с именем student и объявляются переменные stud1 и stud2. Доступ к конкретному элементу(полю) структуры осуществляется с помощью операции ‘точка’(dot). Например: strcpy(stud1.name, “Петров И.И”); Если необходимо содержимое третьего поля переменной stud2 структуры student напечатать, то надо записать: printf(“%s”,stud2.group); Структуры, как и переменные других типов, могут объединяться в массивы структур. Чтобы объявить массив структур, надо сначала задать шаблон структуры( используем имеющийся шаблон student), а затем объявить массив: Struct student stud1kurs[200]; Этот оператор создаст в памяти 200 переменных типа структуры с шаблоном student и именами stud1kurs[0], stud1kurs[1] и т.д. Для доступа к полю kurs 35-го элемента массива используем stud1kurs[34].kurs Если объявлены две переменные типа структуры с одним шаблоном, можно сделать присваивание stud1 = stud2; При этом произойдет побитовое копирование каждого поля одной переменной в соответствующее поле другой переменной. В то же время нельзя использовать операцию присваивания переменных типа структуры, шаблоны которых описаны под разными именами, пусть даже совсем идентично. Тем не менее в этом случае присваивание возможно для отдельных полей, имеющих один и тот же тип данных. 68
Переменная типа структуры может быть глобальной, локальной переменной и формальным параметром. Можно, естественно, использовать структуру или ее поле как любую другую переменную в качестве параметра функции. Например: func1(right.a); или func2(&left.b); Заметим, что & ставится перед именем структуры, а не перед именем поля. Можно в качестве формального параметра передать по значению всю структуру #include <stdio.h> /* Пример 47 */ struct stru{ int x; char y; }; void ff( struct stru param); main(void) { struct stru arg; arg.x=3; arg.y=’a’; ff(arg); } void ff(struct stru param) { printf(“%d %c\n”,param.x,param.y); } Можно также создать указатель на структуру и передавать аргумент типа структуры по ссылке. Объявить указатель на структуру можно следующим образом: struct stru *adr_pointer; Здесь adr_pointer – переменная указатель на структуру struct stru. Если структура передается по значению, то все поля структуры заносятся в стек. Если структура простая и содержит мало элементов, то это не так страшно. Если же структура в качестве своего поля содержит массив, то 69
стек может переполниться. При передаче по ссылке в стек заносится только адрес структуры. При этом копирования структуры не происходит, а также появляется возможность изменять содержимое полей структуры /* Пример 48 */ struct complex{ float x; float y; } c1,c2; struct complex *a; a = &c1; указателю а присваивается адрес переменной с1. Получить значение поля х переменной с1 можно так: (*a).x; Использование указателей на структуру встречается достаточно часто, например, при программировании задач по созданию и обработке динамических связанных структур данных( очереди, стеки, деревья). Поэтому, кроме способа получить значение поля структуры, используя (*a).x можно использовать другой. В языке С вводится специальная операция -> ( стрелка, arrow). Операция стрелка употребляется вместо операции точка, когда мы хотим использовать значение поля структуры с применением переменной указателя. Вместо (*a).x можно использовать a-> x. Этот способ чаще всего и применяется. Завершая разговор о структурах необходимо сказать, что в качестве полей структуры можно использовать массивы, структуры и массивы структур. Пусть объявления переменных имеют вид Struct addr{ Char city[34]; Char street[76]; Int house; }; struct fulladdr{ struct addr addres; int room; char name[48]; } a,b; 70
Здесь addr – шаблон структуры, определенный перед объявлением структуры fulladdr и объявлением переменной а типа структуры fulladdr. Для присвоения значения полю house структуры addres переменной а используем a.adress.house = 234; 6.2. Доступ к отдельным битам В отличие от многих других языков программирования язык С обеспечивает доступ к одному или нескольким битам в байте или слове. В конкретных задачах часто бывает необходимо, чтобы некоторая переменная принимала только два значения. Для этого достаточно использовать только один бит памяти. Такая переменная, по своему содержательному смыслу, является некоторым признаком или флагом. Один из методов, встроенных в язык С и позволяющий иметь доступ к биту, – это поля битов (bit-fields). В действительности поля битов – это специальный тип членов структуры, в котором определено, из скольких бит состоит каждое поле. Основная форма объявления такой структуры следующая: struct имя_структуры { тип имя1: длина в битах; тип имя2: длина в битах; тип имя3: длина в битах; ... тип имяN: длина в битах; }; В этом объявлении структуры тип может быть одним из следующих: int, unsigned, signed. Имя1 может быть пропущено, тогда соответствующее количество бит не используется (пропускается ). Длина структуры в целом всегда кратна восьми. Так, если указать Struct onebit{ Unsigned one_bit: 1; } obj; Здесь для переменной obj будет выделено 8 бит, но использоваться будет только первый. В структуре также могут быть смешаны обычные переменные и поля битов. 71
6.3. Объединения В языке С определен еще один тип для размещения в памяти нескольких переменных разного типа – объединение( union ). Объявляется объединение также как и структура, например /* Пример 49 */ union u{ int i; char ch; long int l }; Это объявление задает шаблон объединения. Можно объявить переменные union u alfa, beta; Можно было объявить переменные одновременно с заданием шаблона. В отличие от структуры для переменной типа union места в памяти выделяется ровно столько, сколько нужно полю объединения, имеющему наибольший размер в байтах. В приведенном выше примере под переменную alfa будет выделено 4 байта памяти. Действительно, поле i требует 2 байта, поле ch – 1 байт, и поле l – 4 байта. Остальные переменные будут располагаться в том же месте памяти. Синтаксис использования полей объединения такой же, как и для структуры: u.ch = ‘N’; Для объединения также разрешена операция ->, если идет обращение к объединению с помощью указателя. Программа, приведенная ниже, выдает на экран двоичный код символа, вводимого с клавиатуры: #include < stdio.h > #include < conio > /* Пример 50 */ struct byte{ int b1: 1; int b2: 1; int b3: 1; int b4: 1; int b5: 1; 72
int b6: 1; int b7: 1; int b8: 1; }; union bits{ char ch; struct byte b; } u; void decode ( union bits b ); main ( void ) { do { b.ch:=getche(); printf(“;”); decode(u); } while ( u.ch != ‘q’); } void decode ( union bits b ) { if ( b.byte.b8 ) printf(“1”); else printf(“0”); if ( b.byte.b7 ) printf(“1”); else printf(“0”); if ( b.byte.b6 ) printf(“1”); else printf(“0”); if ( b.byte.b5 ) printf(“1”); else printf(“0”); if ( b.byte.b4 ) printf(“1”); else printf(“0”); if ( b.byte.b3 ) printf(“1”); else printf(“0”); if ( b.byte.b2 ) printf(“1”); else printf(“0”); if ( b.byte.b1 ) printf(“1”); else printf(“0”); printf(“\n”); } 73
6.4. Перечислимый тип Перечислимый тип ( enumeration ) – это множество поименованных целых констант. Перечислимый тип определяет все допустимые значения которые могут иметь переменные этого типа. Основная форма объявления такого типа следующая: enum имя_типа {список_названий} список переменных; Список переменных может быть пустым. Пример определения перечислимого типа и переменной данного типа enum season { win, spr, sum, aut }; enum season s; Ключом к пониманию сущности перечислимого типа является то, что каждое из имен win, spr, sum, aut представляют собой целую величину. Если эти величины не определены по другому, то по умолчанию они соответсвенно равны нулю, единице, двум и трем. Оператор printf(“%d %d”,win,aut); выдаст на экран числа 0 и 3. Во время объявления типа можно одному или нескольким символам присвоить другие значения, например enum value { one=1,two,three,ten=10,thousand=1000,next}; Если теперь напечатать значения printf(“%d %d %d %d %d\n”,one,two,ten,thousand,next); то на экране появятся числа 1 2 10 1000 1001, т.е. каждый следующий символ увеличивается на единицу по сравнению с предыдущим, если нет другого присваивания. С переменными перечислимого типа можно производить следующие операции: – присвоить переменную типа enum другой переменной того же типа; – провести сравнение с целью выяснения равенства или неравенства; – арифметические операции с константами типа enum (i=win+aut). Нельзя использовать арифметические операции и операции ++ и – для переменных типа enum. Основная причина использования перечислимого типа – улучшение читаемости программ. 74
6.5. Переименование типов Язык С позволяет дать новое название уже существующим типам данных. Для этого используется ключевое слово typedef. При этом создается новый тип данных. Например: typedef char SYMBOL; typedef unsigned UNSIGN; typedef float real; Достаточно часто используется оператор typedef с применением структур: /* Пример 51 */ typedef struct st_tag{ char name[40]; int kurs; char group[5]; int stip; } STUDENT; Теперь для определения переменной можно использовать struct st_tag avar; А можно использовать STUDENT avar;
75
7. ВВОД/ВЫВОД И РАБОТА С ФАЙЛАМИ 7.1. Организация ввода-вывода В языке С отсутствуют специальные операторы ввода-вывода. Все действия, связанные с вводом-выводом, выполняются с помощью функций библиотеки С. Программист может также создать свои собственные функции ввода-вывода на основе библиотечных. При вводе-выводе все данные рассматриваются как поток байтов, связанный либо с файлом на диске, либо с нефайловым физическим устройством (клавиатура, экран монитора, принтер и т. п.). Функции ввода-вывода позволяют выделять из потока и обрабатывать данные различных форматов и размеров, обеспечивая при этом буферизированный форматированный или неформатированный ввод или вывод. Для использования функций ввода-вывода необходимо директивой #include включить файл stdio.h, содержащий объявления функций ввода-вывода, а также определение констант, типов и структур, используемых этими функциями. Открытие потока осуществляется функцией fopen. При успешном открытии эта функция возвращает указатель структуры типа FILE, которая содержит информацию, необходимую для работы с потоком. При ошибке открытия возвращается значение NULL, которое определено как константа в stdio.h. Указатель потока используется в дальнейшем во всех функциях вашей программы, работающих с данным потоком. Количество одновременно открытых потоков ограничевается установками операционной системы. Параметрами функции fopen являются строка, указывающая путь к файлу и его имя, и строка, определяющая тип доступа к потоку. Литерал типа доступа может иметь значения: “r” – для чтения, “w” – для записи, “a” - для записи в конце потока, “r+” – для чтения и записи, “w+” – пустой поток для чтения и записи, “a+” – для чтения и записи в конце потока. Закрытие потока осуществляется функцией fclose. Указатель потока можно позиционировать на любое место в потоке. Для получения текущей позиции в потоке используются функции ftell и fgetpos, для изменения позиции указателя – fseek и fsetpos. В stdio.h определены стандартные указатели потоков: stdin – стандартный ввод(клавиатура), stdout – стандартный вывод(дисплей), stdprn – стандартная печать, stderr 76
– стандартный вывод сообщений об ошибках. Эти указатели можно использовать во всех функциях , требующих указатель файла, без предварительного открытия потока с помощью функции fopen. Кроме того, функции ввода-вывода на терминал (getc, putc и т.п.) не требуют указатель потока, так как используют stdin и stdout. Существуют также функции, реализующие ввод-вывод в текущее окно экрана монитора путем прямой записи в видеопамять ( getch, getche, putch, cprintf и т.д.). Эти функции требуют подключения директивой #include файла conio.h. 7.2. Классификация функций чтения и записи Классификация функций чтения и записи представлена в табл. 7.1. Таблица 7.1 Классификация функций чтения и записи Чтение Объект операции
Последовательность байт Отдельный символ
Форматированные данные
Количество элементов Введенный символ Введенное число Строка Количество введенных полей
fread fgetchar getchar
Данное типа int Строка
Возвращаемое значение
Из потока Из любого Из строки При успешном stdin потока С завершении
fgetc getc getw
gets
fgets
scanf
sscanf Запись
Объект операции
Отдельный символ Строка Форматированные данные
fputc putc putw
puts
fputs
printf
fprintf
0 EOF EOF NULL EOF
Возвращаемое значение
Из потока Из любого Из строки При успешном stdin потока С завершении
Последовательность fputchar байт putchar
При ошибке
sprintf
Выведенный символ Выведенное число Последний символ Количество выведенных байт
При ошибке
EOF EOF EOF EOF
77
Рассмотрим пример программы, которая выводит содержимое файла autoexec.bat в стандартный поток вывода, а также выводит на экран монитора последнии 15 байт этого файла. #include <stdio.h> #include /* Пример 52 */ main(void) { FILE *f; int a; long int n; if((f =fopen(“c:\\autoexec.bat”,”r”))==NULL) /*открываем поток f */ { prinf(“ Ошибка при открытии файла\n”); exit(1); } while((a=fgetc(f)) != EOF /* пока не конец файла читаем в память*/ fputc(a,stdout); /* и выводим в стандартный поток вывода*/ n=-15; fseek(f,n,SEEK_END); /* позиционирование за 15 байт до конца*/ while((a=fgetc(f)) != EOF) /* пока не конец файла читаем в память*/ putchar(a); /* и выводим на экран монитора*/ fclose(f); /* закрываем поток*/ getch(); } 7.3. Функции библиотеки ввода-вывода Библиотека функций ввода-вывода, которые можно использовать при программировании на языке С, весьма разнообразна, что определяет необходимость привести ее полностью. 7.4. Функции для работы с файлами fopen – открывает поток, связанный с файлом filename и типом доступа type. FILE *fopen(char *filename, char *type); fclose – закрывает поток stream. int fclose(FILE *stream); fcloseall – закрывает все открытые потоки. int fcloseall( void ); remove – удаляет файл с именем filename. int remove( char *filename ); rename – переименовывает файл oldname в файл newname. 78
int rename(char *oldname, char *newname); ftell – возвращает положение указателя текущей позиции файла, связанного с потоком stream. Значение возвращается в виде смещения в байтах относительно начала файла. Значение, возвращаемое функцией ftell, в дальнейшем можно использовать при вызове функции fseek. ftell возвращает положение указателя текущей позиции при успешном завершении.При ошибке возвращается значение – 1L. long int ftell( FILE *stream); fseek – устанавливает адресный указатель файла, соответствующий потоку stream, в новую позицию, которая расположена по смещению offset относительно места в файле, определенного параметром fromtwhere. Параметр fromtwhere может иметь одно из трех значений 0, 1 или 2, которые представлены тремя символическими константами, определенными в файле stdio.h, следующим образом: SEEK_SET(0) – начало файла, SEEK_CUR(1) – позиция текущего указателя файла, SEEK_END(2) – конец файла(EOF); Функция fseek возвращает значение 0, если указатель файла успешно перенесен, и ненулевое значение в случае неудачного завершения. int fseek(FILE *stream, long int offset, int fromwhere); fgetpos – сохраняет позицию указателя файла, связанного с потоком stream, в месте, указываемом параметром pos. При успешном завершении fgetpos возвращает 0. int fgetpos(FILE *stream, fpos_t *pos); Здесь и далее fpos_t - предварительно объявленный тип typedef long fpos_t. fsetpos - устанавливает указатель текущей позиции файла, связанного с потоком stream в новую позицию, которая определяется значением, получаемым предшествующим вызовом функции fgetpos. При успешном завершении fsetpos возвращает 0. Int fsetpos( FILE *stream, const fpos_t *pos); 7.5. Функции неформатированного ввода-вывода fgetc – получает символ из потока stream. int fgetc(FILE *stream); fgetchar – получает символ из потока stdin. int fgetchar( void ); 79
fgets – получает строку s длиной не более n символов из потока stream. char *fgets(char *s, int n, FILE *stream); fputc – выводит символ с в поток stream. int fput(int c, FILE *stream); fputchar- выводит символ c в поток stdout. int fputchar(int c); fputs – выводит строку символов string в поток stream. int fputs(char *string, FILE *stream); gets – получает строку символов s из потока stdin. char *gets(char *s); getc – выводит из потока stream символ этого потока. int getc(FILE *stream); getchar – выводит символ из потока stdin. int getchar( void ); putc – выводит символ c в поток stream. int putc(int c, FILE *stream); putchar – выводит символ с в поток stdout. int putchar(int c); puts – выводит строку s в поток stdout. int puts(const char *s); putw – помещает в поток stream целое значение w. int putw(int w, FILE *stream); getw – вводит из потока stream целое число. int getw(FILE *stream); 7.6. Функции блочного ввода-вывода fread – считывает n элементов данных длиной size из потока stream по адресу ptr. size_t fread(void *ptr, size_t size, size_t n, FILE *stream ); Здесь и далее size_t – предварительно объявленный в библиотеке тип typedef usigned size_t. fwrite – записывает n элементов данных длиной size из ptr и поток stream. Size_t fwrite( void *ptr, size_t size, size_t n, FILE *stream); 80
7.7. Функции форматированного ввода-вывода printf – производит форматированный вывод в stdout. int printf(const char *format [,argument,…]); scanf – выполняет форматированный вывод из потока stdin. int scanf(const char *format [,adress,…]); fprintf – посылает форматированный вывод в поток stream. int fprintf(FILE *stream, const char *format [,argument,…]); fscanf – выполняет форматированный ввод из потока stream. int fscanf(FILE *stream, const char *format [,adress,…]); sprintf – производит форматированный вывод в сстроку buffer. int sprintf(char *buffer, const char *format [,argument,…]); sscanf – выполняет форматированный ввод из строки buffer. Int sscanf(const char *buffer, const char *format [,adress,…]); 7.8. Функции ввода-вывода на экран(conio.h) cprintf – осуществляет форматированный вывод на экран. int cprintf(const char *format [,argument,…]); Таблица 7.2 Файлы и их назначение Заголовочный файл
assert.h ctype.h errno.h float.h limits.h locale.h match.h setjmp.h signal.h stdarg.h stddef.h stdio.h stdlib.h string.h time.h
Назначение
Диагностика программ Преобразование и проверка символов Проверка ошибок Работа с числами с плавающей точкой Определение размеров целочисленных типов Поддержка интернациональной среды Математическая библиотека Возможность нелокальных переходов Обработка сигналов Поддержка функций с неопределенным числом параметров Разное Библиотека стандартного ввода-вывода Функции общего назначения Функции работы со строками символов Функции работы с датами и временем
81
getch – читает один символ с консоли без вывода его на экран. int getch(void); getche – cчитывает один символ с консоли и отображает его в текущем текстовом окне экрана. int getche( void ); putch – выводит символ на экран. int putch(int c); Каждая библиотечная функция , определенная стандартом языка С, имеет прототип в соответствующем заголовочном файле. В соответствие со стандартом ANSI языка С должно быть 15 следующих заголовочных файлов (табл. 7.2). На самом деле каждый из компиляторов содержит , как правило больше заголовочных файлов, например – библиотеку функций работы с графическим экраном graphics.h и библиотеку функций для работы с текстовым экраном conio.h.
82
Библиографический список 1. Трой Д. А. Программирование на языке Си для персонального компьютера IBM PC/ Пер.с англ. В. А. Кузьмина; Под ред. И. В. Емелина. М.: Радио и связь, 1991. 429 с. 2. Уинер Р. Язык Турбо Си/ Пер. с англ. М. П. Матенина; Под ред. В. В. Мартынюка. М.: Мир 1991. 384 с. 3. Уэйт М. и др. Язык Си: Руководство для начинающих / М.Уэйт, С. Прата, Д. Мартин; Под ред. Э. А. Трахтенгерца. М.: Мир, 1988. 512 с. 4. Березин Б. И., Березин С. Б. Начальный курс С и С++. М.: ДиалогМИФИ, 1999. 288 с.
83
Оглавление Предисловие ............................................................................................. 1. Основные понятия языка С ................................................................ 1.1. Алфавит, идентификаторы, ключевые слова, комментарии .. 1.2. Примеры простых программ ...................................................... 1.3. Определение некоторых понятий .............................................. 2. Переменные, константы, операции и выражения ........................... 2.1. Базовые типы данных и объявление переменных ................... 2.2. Основная форма объявления переменных ................................ 2.3. Константы .................................................................................... 2.4. Символьные переменные и строки .......................................... 2.5. Инициализация переменных ..................................................... 2.6. Операции ..................................................................................... 2.7. Выражения .................................................................................. 3. Операторы ............................................................................................. 3.1. Условный оператор ..................................................................... 3.2. Оператор множественного выбора ............................................ 3.3. Операторы циклов ....................................................................... 3.4. Операторы break и continue ........................................................ 3.5. Оператор безусловного перехода ............................................... 4. Массивы и указатели ........................................................................... 4.1. Понятие массива, объявление массива ..................................... 4.2. Массивы символов, строки. Функции работы со строками .. 4.3. Двумерные массивы .................................................................... 4.4. Инициализация массивов ........................................................... 4.5. Указатели, объявление указателей, операции над указателями ........................................................................... 4.6. Связь указателей и массивов ..................................................... 4.7. Массивы указателей .................................................................... 5. Функции ................................................................................................ 5.1. Объявление функций. Оператор return .................................... 5.2. Прототипы функций ................................................................... 5.3. Область действия и область видимости ................................... 5.4. Классы памяти ............................................................................. 5.5. Параметры и аргументы функций ............................................. 5.6. Рекурсивные функции ................................................................ 5.7. Указатель на функцию ................................................................ 84
3 4 4 5 8 9 9 11 13 16 18 18 25 27 27 29 32 35 36 37 37 38 41 42 43 47 48 51 51 53 54 56 61 64 65
6. Типы данных, определяемые пользователем ................................... 6.1. Структура .................................................................................... 6.2. Доступ к отдельным битам ....................................................... 6.3. Объединения ............................................................................... 6.4. Перечислимый тип ..................................................................... 6.5. Переименование типов .............................................................. 7. Ввод/вывод и работа с файлами ......................................................... 7.1. Организация ввода-вывода ......................................................... 7.2. Классификация функций чтения и записи ............................. 7.3. Функции библиотеки ввода-вывода .......................................... 7.4. Функции для работы с файлами ................................................ 7.5. Функции неформатированного ввода-вывода ......................... 7.6. Функции блочного ввода-вывода .............................................. 7.7. Функции форматированного ввода-вывода ............................. 7.8. Функции ввода-вывода на экран(conio.h) ................................ Библиографический список ....................................................................
67 67 71 72 74 75 76 76 77 78 78 79 80 81 81 83
85
Учебное издание
Кучин Николай Валентинович Павлова Марина Михайловна
ОСНОВЫ ПРОГРАММИРОВАНИЯ НА ЯЗЫКЕ СИ Учебное пособие
Редактор А. В. Семенчук Компьютерная верстка А. Н. Колешко Лицензия ЛР №020341 от 07.05.97. Сдано в набор 11.09.01. Подписано к печати 04.10.01. Формат 60×84 1/16. Бумага тип. №3. Печать офсетная. Усл. печ. л. 4,65. Усл. кр.-отт. 4,77. Уч. -изд. л. 5,0. Тираж 100 экз. Заказ № Редакционно-издательский отдел Лаборатория компьютерно-издательских технологий Отдел оперативной полиграфии СПбГУАП 190000, Санкт-Петербург, ул. Б. Морская, 67