Всё для Учёбы — студенческий файлообменник
1 монета
doc

Студенческий документ № 081056 из СИЮ

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

Содержание.

1. Лекция: Общее знакомство.

-Происхождение языка Си.

-Достоинства языка Си.

-Будущее языка Си.

-Использование языка Си.

-Использование текстового редактора для подготовки программ.

-Исходные и выполняемые файлы. Примеры простой программы.

-Пример простой программы на языке Си.

-Структура простой программы.

2. Лекция: Данные, символьные строки, директива #define

-Основные типы данных.

-Описание различных типов, переменные и константы.

-Символьные строки.

-Препроцессор языка Си.

3. Лекция: Операции.

-Основные операции.

-Дополнительные операции.

-Перечень операций языка Си.

4. Лекция: Операторы.

-Выражения.

-Простейшие выражения.

-Операторы.

-Составные операторы.

5. Лекция: Преобразование типов.

-Эквивалентность типов.

-Преобразование типов.

-Неявное преобразование типа.

-Арифметические преобразования.

-Явное преобразование типов.

-Синтаксис типов.

6. Лекция: Функции и переключение ввода-вывода.

-Ввод и вывод одного символа.

-Буферы. -Чтение одной строки.

-Чтение файла.

-Переключение и работа с файлами.

7. Лекция: Выбор вариантов.

-Выбор вариантов.

-Оператор if.

-Расширение оператора if.

-Операции отношения.

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

-Операция условия: ?:.

-Множественный выбор.

8. Лекция: Циклы и другие управляющие средства. Структурное программирование.

-Цикл с предусловием.

-Цикл со счетчиком.

-Цикл с постусловием.

-Другие управляющие операторы.

-Структурное программирование.

9. Лекция: Функции.

-Создание и использование функций.

-Аргументы функции.

-Возвращение значений.

-Локальные переменные.

-Нахождение адресов.

-Указатели, первое знакомство.

-Функции с переменным количеством аргументов.

10. Лекция: Классы памяти и разработка программ.

-Классы памяти и область действия.

-Автоматические переменные.

-Внешние переменные.

-Статические переменные.

-Внешние статические переменные.

-Регистровые переменные.

11. Лекция: Препроцессор языка Си.

-Общие сведения.

-Символические константы: #define.

-Использование аргументов с #define.

-Макроопределение или функция?

-Включение файла #include.

-Условная компиляция.

12. Лекция: Массивы и указатели.

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

-Массивы.

-Указатели.

-Динамические объекты.

-Создание динамических объектов.

-Доступ к динамическим объектам.

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

-Инициализация массивов и классы памяти.

-Функции.

-Операции с указателями.

13. Лекция: Символьные строки и функции над ними.

-Строковые константы.

-Массивы символьных строк и их инициализация.

-Массив и указатель: различия.

-Указатели и строки.

-Ввод-вывод строк.

-Обработка строк.

14. Лекция: Структуры.

-Определение структурных переменных.

-Доступ к компонентам структуры.

-Поля битов в структурах.

-Объединения.

-Перечисления.

-Переменные структуры.

-Указатели структуры.

-Массив структур.

-Переименования типов.

15. Лекция: Библиотека языка Си и файлы ввода-вывода.

-Библиотека языка Си содержит множество функций и макроопределений.

-Библиотеки меняются от системы к системе, но есть ядро функций (стандартная библиотека).

-Распределение памяти.

16. Лекция: Функции в примерах.

-Функция получения случайных чисел.

-Поиск узлов из простых чисел.

-Матрица инцидентности.

-Структуры данных.

-Работа с файлами.

-Все операции со стеком.

-Примеры из графики, все преобразования трехмерного пространства.

1. Лекция: Общее знакомство.

-Происхождение языка Си.

-Достоинства языка Си.

-Будущее языка Си.

-Использование языка Си.

-Использование текстового редактора для подготовки программ.

-Исходные и выполняемые файлы. Примеры простой программы.

-Пример простой программы на языке Си.

-Структура простой программы.

Происхождение языка Си.

Язык программирования Си был разработан и реализован в 1972 году сотрудником фирмы AT&T Bell Laboratories Денисом Ритчи. Прообразом языка Си для Д. Ритчи послужил язык Би, разработанный Кеном Томпсоном. Он является результатом эволюционного развития языков BCPL (Richards, M., "BCPL: A. Tool for Compiler Writing and System Programming", Proc. AFIPS SJCC, 34, 557-566, 1969) и Би (Johnson, S. C., and B. W. Kernighan, "The Programming Language B", Comp. Sci. Tech. Rep. No. 8, Bell Laboratories. 1973). Основным достоинством языка Си по сравнению с языками BCPL и Би является введение в него типов данных. Язык Си был разработан во время создания операционной системы UNIX (OC UNIX).

Достоинства языка Си.

Особое значение придается гибкости. Язык Си компактен, является относительно маленьким языком программирования. Ввод-вывод не считается частью языка Си, а определяется стандартной библиотекой. Вседозволенность в языке Си является следствием желания как можно больше расширить область его применения. Язык Си удалось сделать относительно маленьким языком программирования за счет того, что в его состав не были включены ввод-вывод и средства для работы со строками. Язык Си был задуман настолько гибким, что эти возможности могли быть реализованы в каждом конкретном случае наиболее удачным образом. Практический опыт использования языка Си показал правильность такого подхода. Большая часть операционной системы UNIX и все утилиты этой операционной системы, включая и несколько трансляторов с языка Паскаль, реализованы на языке Си. Язык программирования является языком программирования с сильной типизацией, если:

* каждый объект в этом языке программирования принадлежит точно одному из существующих в этом языке программирования типу данных;

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

* преобразование типов не производится путем трактовки представления значения как данных различных типов.

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

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

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

Си - эффективный язык. Его структура позволяет наилучшим образом использовать возможности современных ЭВМ. Написанные на языке Си программы обычно отличаются компактностью и быстротой исполнения.

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

Си - мощный и гибкий язык. Например, большая часть мощной и гибкой OC UNIX написана на языке Си. Речь идет о компиляторах и интерпретаторах других языков, таких, как Фортран, АПЛ, Паскаль, Лисп, Лого и Бейсик. Кроме того, программы, написанные на Си, используются для решения физических и технических проблем, компьютерной графики и даже производства мультипликационных фильмов.

Си - обладает рядом конструкций управления, обычно ассоциируемых с ассемблерами.

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

Будущее языка Си.

Многие фирмы, производящие программное обеспечение, все чаще обращаются к Си как к удобному языку для реализации своих проектов, поскольку известно, что Си позволяет получить компактные и эффективные программы. И эти программы могут быть легко модифицированы и адаптированы к новым моделям ЭВМ! Языки программирования как С++, Java, С#, UML и т.д. имеют "сишную" семантику. Си используется фирмами, производящими программное обеспечение, студентами, обучающимися программированию. И если вы хотите работать в сфере программотехники, то один из первых вопросов, на который вы должны будете отвечать "Да" - это вопрос "Умеете ли Вы программировать на Си?"

Исходные и выполняемые файлы

Приведем простенькую программу на языке Си:

#include

main( ) { printf("Добро пожаловать!\n");

} Все конструкции языка мы рассмотрим в последующих лекциях. Если эту программу оттранслировать, то получим файл с выполняемой программой. В результате работы этой программы на дисплей будет выведено предложение: "Добро пожаловать!". Наша программа, несмотря на свою лаконичность и простоту, для компьютера является совершенно бессмысленным набором символов, так как он не понимает директив #include или printf. Компьютер понимает только специальный язык, называемый машинным кодом, т.е. набор последовательностей двоичных цифр, например 101000101. Если мы хотим, чтобы компьютер выполнил программу, мы должны осуществить перевод (трансляцию) кода, написанного на Си (исходного) в ее код (машинный). В результате этих действий будет получен выполняемый файл. Процесс перевода (трансляции) удалось переложить на сам компьютер. Программы, переводящие исходный код в машинный код, называются компиляторами. Детали процесса перевода зависят от особенностей конкретной системы.

Пример простой программы на языке Си

Рассмотрим простую программу на языке Си:

#include main( )/*простая программа*/

{ int courses;

courses=30; printf("Сколько учебных курсов на сайте");

printf(" www.intuit.ru?\n");

printf("Более %d. Но будет еще больше!\n",

courses);

} Давайте, выполним эту программу. Сначала используем текстовый редактор для создания файла, содержащего текст программы. Этому файлу нужно присвоить какое-то имя. Допустим - intuit.c . Выполним компиляцию программы. После запуска программы, при условии отсутствия синтаксических ошибок, результат должен выглядеть следующим образом:

Сколько учебных курсов на сайте www.intuit.ru?

Более 30. Но будет еще больше!

Пояснения к программе

Мы выполним два просмотра текста программы: во время первого просмотра объясним смысл каждой строки, а во время второго - рассмотрим детали.

Первый просмотр

#include - включение другого файла.

Эта строка указывает компилятору, что нужно включить информацию, содержащуюся в файле stdio.h.

main( ) - имя функции

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

/* простая программа*/ - комментарий

/*, */ - открывающая и закрывающая скобки многострокового комментария.

Комментарии - это примечания, помогающие понять смысл программы. Они предназначены для читателя, и игнорируются компилятором.

{ - начало тела функции

Открывающая фигурная скобка отмечает начало последовательности операторов - тело, или определение, функции. Конец определения отмечается закрывающей фигурной скобкой - }.

int courses; - оператор описания.

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

courses = 30; - оператор присваивания

Этот оператор служит для присваивания переменной courses значения 30.

printf("Сколько учебных курсов на сайте"); - оператор вывода на печать

С его помощью выводится на печать фраза, заключенная в кавычки:

Сколько учебных курсов на сайте

printf(" www.intuit.ru?\n"); - еще один оператор вывода на печать. Этот оператор добавляет слова www.intuit.ru? в конец последней печатаемой фразы. Комбинация символов \n указывает компилятору на начало новой строки.

printf("Более %d. Но будет еще больше!\n",courses); - этот оператор выводит на печать значение переменной courses, равное 30, содержащееся в кавычках. Символы %d указывают компилятору, где и в какой форме печатать значение переменной courses.

} - конец Программа завершается закрывающей фигурной скобкой.

Второй просмотр

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

standard input/output header - заголовок стандартного ввода-вывода.

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

Строка "#include " даже не является оператором языка Си. Символ # указывает, что она должна быть обработана "препроцессором" языка Си. Препроцессор осуществляет некоторую предварительную обработку текста программы перед началом компиляции.

main( )

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

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

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

/* простая программа*/

Комментарии облегчают процесс понимания программы. Длинный комментарий может помещаться на отдельной строке или даже занимать несколько строк. Все, что находится между символом, указывающим на начало комментария /*, и символом, указывающим на его конец */, игнорируется компилятором.

{,} Фигурные скобки { } отмечают начало и конец тела функции.

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

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

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

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

Идентификатор переменной - имя переменной. Для обозначения имени переменной разрешается использовать строчные и прописные буквы, цифры и символ подчеркивания, считающийся буквой. Первым символом должна быть обязательно буква.

Например, courses, cat_1, _total - правильные идентификаторы, а $courses*, 1cat, -total - неправильные.

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

не описать все используемые переменные.

courses=30; Оператор присваивания является одним из основных средств языка. Приведенную строку программы можно интерпретировать так: присвоить переменной courses значение 30. При описании переменной courses была выделена ячейка памяти, и только теперь в результате выполнения оператора присваивания переменная получает свое значение. При желании мы могли бы присвоить ей другое значение, поэтому имя courses и обозначает переменную.

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

Символы \n служат директивой начать новую строку на устройстве вывода. Комбинация \n представляет один символ, называемый "новая строка". Его смысл формулируется так: начать вывод новой строки с самой левой колонки. Символ "новая строка" служит одним из примеров того, что называется "управляющей последовательностью". Управляющая последовательность начинается с "\".

2. Лекция: Данные, символьные строки, директива #define.

-Основные типы данных.

-Описание различных типов, переменные и константы.

-Символьные строки.

-Препроцессор языка Си.

Основные типы данных

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

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

int long short

unsigned

char float double

Первые четыре ключевых слова используются для представления целых, т.е. целых чисел без десятичной, дробной части. Если мы хотим подчеркнуть, что целое не может быть отрицательным, то нужно к целому подписывать ключевое слово unsigned, например, unsigned short. char предназначено для указания на буквы и другие символы. float, double используются для представления чисел с десятичной точкой. Типы, обозначаемые этими ключевыми словами, можно разделить на два класса по принципу размещения в памяти машины. Первые пять ключевых слов определяют целые типы данных, последние два - типы данных с плавающей точкой. Дадим краткое объяснение их смысла. Термины бит, байт, слово используются для описания как элементов данных, которые обрабатывает компьютер, так и элементов памяти. Рассмотрим эти понятия относительно памяти. Наименьшая единица памяти называется бит. Она может принимать одно из двух значений: 0 или 1. Байт в большинстве машин состоит из 8 бит. Всего в байтовом формате можно представить 256 (два в восьмой степени) различных комбинаций из нулей и единиц. Эти комбинации можно использовать для представления целых чисел в диапазоне от 0 до 255 или для кодирования набора символов. Слово является естественным элементом памяти. Есть ЭВМ, у которых слово равно 8 битам, 16 битам, 32 битам или 64 битам.

Описание различных типов, переменные и константы

Целые числа.

У целого числа никогда не бывает дробной части. Представив целое число в двоичном виде, его нетрудно разместить в машине. Например, число 3 в двоичном виде выглядит как 11. Если его поместить в слово 32-разрядной машины, необходимо первые 30 бит установить в 0, а последние 2 бита - в 1.

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

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

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

Описание данных целого типа.

При описании данных необходимо написать только тип, за которым должен следовать список имен переменных. Например, int dog, rad, nina. В качестве разделителя между именами переменных необходимо использовать запятую.

Целые константы.

Согласно правилам языка Си, число без десятичной точки и без показателя степени рассматривается как целое. Поэтому компилятор по записи константы определяет, целая она или вещественная. Если нужно ввести константу типа long, то нужно указать признак L или l в конце числа. Если при записи константы целое начинается с цифры 0, то эта константа интерпретируется как восьмеричное число, если же целое начинается с символа 0x или 0X - как шестнадцатеричное число.

Инициализация переменных целого типа.

Константы применяются при инициализации переменных. Это означает присваивание переменной некоторого значения перед началом обработки. Можно инициализировать переменную в операторе описания.

Например,

int dog=5; int rad=077;

int nina=0X99;

! В языке Си введено три класса целых чисел, имеющих различные размеры. Тем самым пользователю языка Си предоставили возможность выбора типа переменной с требованием задачи. Например, если переменная типа int занимает одно слово, а переменная типа long занимает два слова, значит, тип long позволяет обрабатывать большие числа. Если в задаче не используются большие числа, то незачем вводить в программу переменные типа long, т.к. если вместо числа, занимающего одно слово в памяти, используется число, занимающее два слова, работа машины замедляется.

Описание данных типа unsigned.

Этот тип является модификатором типов: int, short, long. Мы можем использовать комбинацию ключевых слов unsigned int, unsigned short, unsigned long, т.е. переменная не может принимать отрицательного значения. Для указания типа unsigned int достаточно написать unsigned. Целые беззнаковые константы записываются так же, как и обычные константы, запрещено только использование знака минус.

Например, unsigned age;

Описание данных типа char.

Этот тип определяет целые числа без знака в диапазоне от 0 до 255. Такое целое обычно размещается в одном байте памяти. Для описания символьной переменной применяется ключевое слово char. Правила описания более чем одной переменной и инициализации переменных остаются теми же, что и для других основных типов.

Например, char dog, cat;

Символьные константы. Символы в языке Си заключаются в апострофы.

Например, char dog; dog='b';

Если апострофы опущены, то компилятор считает, что используется неописанная переменная b. В стандартном языке Си значением переменной или константы типа char могут быть только одиночные символы.

Примеры символьных констант: 'A', 'a', '7', '$'.

Специальные (управляющие) символьные константы.

Новая строка (перевод строки) '\n' Горизонтальная табуляция '\t' Вертикальная табуляция '\v' Возврат на шаг '\b' Возврат каретки '\r' Перевод формата '\f' Обратная косая '\\' Апостроф '\'' Кавычки '\"' Нулевой символ (пусто) '\0' Кроме того, любой символ может быть представлен последовательностью трех восьмеричных цифр: '\ddd'. Символьные константы считаются данными типа int.

Строковые константы.

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

Например,

"Это строковая константа"

В конце каждой строки компилятор помещает нулевой символ '\0', отмечающий конец данной строки. Каждая строковая константа, даже если она идентична другой строковой константе, сохраняется в отдельном месте памяти. Если необходимо ввести в строку символ кавычек ("), то перед ними надо поставить символ обратной косой (\). В строку могут быть введены любые специальные символьные константы, перед которыми стоит символ \. Символ \ и следующий за ним символ новой строки игнорируется.

Перечисляемые константы.

Имена, указанные в описании перечисляемых констант, трактуются как целые числа (см. описание перечисления).

Описание данных типа float и double.

Числа с плавающей точкой в языке Си описываются типом float. Числа с плавающей точкой аналогичны числам в обычной алгебраической записи, используемой при работе с очень большими или малыми числами.

Пример алгебраической записи чисел с плавающей точкой:

Число Алгебраическая запись Запись для ввода в машину 5000 = 5.0 * 103 = 5.0e3 0.000077 = 7.7 * 10-5 = 7.7e-5 Обычно для размещения в памяти числа с плавающей точкой отводится 32 бита - 8 бит для представления порядка и знака и 24 бита - для мантиссы, т.е. коэффициента при степени десяти. Для представления данных типа double (вычисление с двойной точностью) для представления чисел используется удвоенное число битов. Другой способ определения данных типа double заключается в использовании ключевых слов long float.

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

Например,

float dog, cat, bigword=5.77e+34;

Константы с плавающей точкой.

В языке Си имеется несколько возможностей записи констант с плавающей точкой. Наиболее общая форма записи константы - это последовательность десятичных цифр со знаком, включающим в себя десятичную точку, затем символ e или Е и показатель степени по основанию 10 со знаком. Знак "+" можно не писать. Ниже приведено несколько правильно записанных констант с плавающей точкой:

1.1e+12

3.14159 ! Во время разработки программы необходимо составить список требуемых переменных и указать при этом, какого они должны быть типа. Описывайте эти данные в самом начале тела функции, в которой они используются. Имена переменных выбирайте таким образом, чтобы они указывали на их смысл. При инициализации переменной следите за тем, чтобы тип константы соответствовал типу переменной, хотя язык Си рассматривает такие несоответствия менее жестко, чем Паскаль, но лучше учиться избегать дурного тона в программировании!

Символьные строки

Символьная строка - это последовательность символов, возможно пустая (""). Рассмотрим пример:

"Большой спрос на образование в области

информационных дисциплин объясняется не только

популярностью компьютеров в современном обществе,

но и реальной пользой от их применения."

Кавычки не являются частью строки. Они вводятся только для того, чтобы отметить ее начало и конец. В языке Си нет специального типа, который можно было бы использовать для описания строк. Вместо этого строки представляются в виде "набора" элементов типа char. Это означает, что символы в строке можно представить расположенными в соседних ячейках памяти - по одному символу в ячейке. Символ \0 в языке Си используется для того, чтобы отмечать конец строки. Нуль-символ не выводится на печать и в таблице кода ASCII (American Standard Code for Information Interchange) имеет номер 0. Наличие нуль-символа означает, что количество ячеек массива символов должно быть на одну больше, чем число символов строки. Массив можно представить как совокупность нескольких ячеек памяти, объединенных в одну строку. Массив - это упорядоченная последовательность элементов данных одного типа. В нашем примере мы создали массив из 177 ячеек памяти, в каждую из которых можно поместить один символ типа char. Это можно сделать с помощью оператора описания:

char String[177];

Квадратные скобки указывают, что переменная String - массив из 177 элементов, а char задает тип каждого элемента. Длину строки в символах (без завершающего символа) определяет функция strlen( ). Обращение к ней в нашем примере выглядит так:

strlen(String); Результат - целое число.

Препроцессор языка Си

Вернемся к константам. Чтобы ввести ту или иную константу в программу, нужно указать ее фактическое значение, как было сказано выше. Можно использовать вместо этого "символические константы" и позволить компилятору заменить символические константы числами. Как можно создать такую константу? Можно это сделать так:

float cost = 0.0012;

Такой способ задания констант в больших программах неэкономичен. В Си имеется другой, лучший способ. Этот способ реализуется с помощью препроцессора языка Си. Препроцессор дает возможность задавать константы. Для этого в начало программы нужно добавить строку, аналогичную следующей:

#define COST 0.0012

При компиляции программы каждый раз, когда появится переменная COST, она будет заменяться величиной 0.0012. Такой процесс называется подстановкой во время компиляции. Замечание по поводу формата: сначала идет ключевое слово #define (оно должно начинаться с самой левой позиции), потом идет символическое имя константы, а затем ее величина. Символ "точка с запятой" не используется, потому что это не оператор языка Си.

Если в качестве первого символа в строке программы используется символ #, то эта строка является командной строкой препроцессора (макропроцессора). Командная строка препроцессора заканчивается символом перевода на новую строку. Если непосредственно перед концом строки поставить символ обратной косой (\), то командная строка будет продолжаться на следующей строке программы.

Препроцессор используется для обработки текста программы до этапа ее компиляции. Обычно препроцессоры служили средством расширения языков с целью обеспечения дополнительных возможностей. Несмотря на бесчисленное множество препроцессоров, созданных для расширения возможностей языков программирования, все они были нестандартными. Для некоторых языков, например для языков общего назначения ПЛ/1 и Си, препроцессоры поставлялись как часть их стандартной среды. Препроцессор для языка Си обеспечивает средства для определения макросов, определения констант, включения файлов и условную компиляцию. Препроцессор языка Си вызывается автоматически при обращении к компилятору. Программа может быть обработана только препроцессором без компиляции, если в команде сс указать ключ -Е:

cc -E имя_файла

Результат работы препроцессора помещается в поток стандартного вывода stdout. Обработка программы препроцессором без компиляции позволяет программисту проанализировать действие определений препроцессора и макровызовов.

! Символическую константу после #define лучше писать прописными буквами. В процессе использования языка Си выработалась традиция писать константы большими буквами. Если при просмотре программы встречается имя, написанное прописными буквами, сразу становится ясно, что это константа, а не переменная. Давайте не нарушать традицию!

Директиву #define можно использовать для определения символьных и строковых констант. В первом случае необходимо использовать "апостроф", а во втором кавычки.

Например,

#define NULL '\0'

#define USA '$' #define RUSSIA "Рубль"

#undef USA Команда #undef USA отменяет предыдущее определение для идентификатора USA. Через команду #define можно задавать выражения - макросы, которые вычисляются и при компиляции подставляются в программу. Воизбежании ошибок при вычислении выражений макроопределения необходимо заключать в скобки:

#define идентификатор1 (идентификатор2,_) строка

Пример: #define abs(A) (((A)>0) ? (A): - (A))

Каждое вхождение выражения abs(arg) в тексте программы заменяется на ((arg)>0) ? (arg): -(arg)), причем параметр макроопределения A заменяется на arg.

Пpимер: #define nmem(P,N)\

(P)->p_mem[N].u_long

Символ \ продолжает макроопределение на вторую строку. Это макроопределение уменьшает сложность выражения, описывающего массив объединений внутри структуры.

Макроопределения препроцессора языка Си имеют две формы - простую и параметризованную,

#define идентификатор строка_замены

#define идентификатор(x1,x2,_,xn) строка_замены

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

Замечание.

Командная строка #include может встречаться в любом месте программы, но обычно все включения размещаются в начале исходного текста:

#include

Пример: #include

Препроцессор заменяет эту строку содержимым файла math.h. Угловые скобки обозначают, что файл math.h будет взят из некоторого стандартного каталога, обычно это /usr/include. Текущий каталог не просматривается:

#include "имя_файла"

Пример:

#include "ABC" Препроцессор заменяет эту строку содержимым файла ABC. Так как имя файла заключено в кавычки, то поиск производится в текущем каталоге, в котором содержится основной файл исходного текста. Если в текущем каталоге данного файла нет, то поиск производится в каталогах, определенных именем пути в опции -i препроцессора. Если и там файла нет, то просматривается стандартный каталог.

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

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

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

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

* Обеспечивается возможность принятия решений во время компиляции, а не во время выполнения. Часто это оказывается более эффективным подходом.

Для условной компиляции используется инструкция препроцессора if. Она имеет две формы - с частью else и без нее:

#if - заголовок

текстовые строки для случая "истина"

#endif и #if - заголовок

текстовые строки для случая "истина"

#else

текстовые строки для случая "ложь"

#endif где if - заголовок является управляющей строкой препроцессора, а текстовые строки могут содержать произвольный текст. Управляющая строка препроцессора if-заголовка содержит условие, на основе анализа которого производится выбор соответствующих текстовых строк:

#if константное_выражение

Пример:

#ifdef ABC Истина, если идентификатор ABC определен ранее

командой #define.

#ifndef идентификатор

Пример:

#ifndef ABC Истина, если идентификатор ABC не определен

в настоящий момент.

#else ... #endif

Если предшествующие проверки #if, #ifdef или #ifndef дают значение Истина, то строки от #else до #endif игнорируются при компиляции. Если эти проверки дают значение Ложь, то строки от проверки до #else, а при отсутствии #else - до #endif, игнорируются. Команда #endif обозначает конец условной компиляции.

Пример:

#ifdef DEBUG fprintf (stderr, "location. X=%d\n",x);

#endif При помощи команд препроцессора можно изменить номер текущей строки или имя компилируемого файла:

#line целая_константа "имя_файла"

Пример:

#line 20 "ABC" Препроцессор изменяет номер текущей строки и имя компилируемого файла. Имя файла может быть опущено.

Замечание.

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

3. Лекция: Операции.

-Основные операции.

-Дополнительные операции.

-Перечень операций языка Си.

Основные операции

Рассмотрим способы обработки данных - для этого язык Си имеет широкий набор возможностей. Основные арифметические операции: сложения, вычитания, умножения, деления. Операции в языке Си применяются для представления арифметических действий. Например, выполнение операции + приводит к сложению двух величин, стоящих слева и справа от этого знака. Рассмотрим операции =, +, -, *, /. В языке Си нет операции возведения в степень.

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

В языке Си знак равенства не означает "равно". Он означает операцию присваивания некоторого значения. С помощью оператора

yar=2004; переменной c именем yar присваивается значение 2004, т.е. элемент слева от знака = - это имя переменной, а элемент справа - ее значение. Мы называем символ = операцией присваивания. В этой операции действие выполняется справа налево.

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

i=i+1;

C математической точки зрения это бессмыслица. Если вы прибавляете единицу к конечному числу, результат не может быть равен исходному числу. Но как оператор присваивания, данная строка имеет вполне определенный смысл, который можно выразить, например, такой фразой. Взять значение переменной с именем i, к нему прибавить 1, а затем присвоить новое значение переменной с именем i.

Оператор вида

2004=yar; на языке Си не имеет смысла, поскольку 2004 - число. Мы не можем присвоить константе какое-то значение; ее значением является она сама. Поэтому, помните, что элемент, стоящий слева от знака =, всегда должен быть именем переменной. Операнд - это то, над чем выполняются операции. Например, можно описать процесс "поедания" картошки как применения операции "поедание" к операнду "картошка".

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

/* таблица результатов по шахматам */

main( ) { int l, m, n;

n=m=l=165; printf("l m n\n");

printf("Счет первой партии %4d %8d %8d\n", l, m, n);

} Присваивания выполняются справа налево: сначала переменная l получает значение 165, затем переменная m и наконец n. В языке Си имеется несколько других операций присваивания, которые отличаются от описанной операции. Их мы рассмотрим попозже.

Представим еще один пример простой программы:

/* использование операции присваивания */

main( ) { /* переменные number, ouzo, cost инициализируются конкретными значениями*/

int number=5;

float ouzo=13.5;

int cost=31000;

printf("number ouzo cost\n");

printf("number=%d ouzo=%f cost=%d\n",

number, ouzo, cost);

} Операция сложения: +

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

printf("%d", 100 + 65);

на печать будет выведено число 165, а не выражение 100+65. Операнды могут быть как переменными, так и константами. Операция + называется "бинарной", или "диадической". Эти названия отражают тот факт, что она имеет дело с двумя операндами.

Пример:

i=j+2; Переменной i присваивается значение переменной j плюс 2.

Операция вычитания: -

Выполнение операции вычитания приводит к вычитанию числа, расположенного справа от знака -, из числа, стоящего слева от этого знака. Оператор

n = 163.00 - 100.00;

присваивает переменной n значение 63.00.

Операция изменения знака: -

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

teg = -15;

get = -teg; переменной get будет присвоено значение 15. Когда знак используется подобным образом, данная операция называется "унарной". Такое название указывает на то, что она имеет дело только с одним операндом.

Пример: x = -x;

Операция изменяет алгебраический знак x.

Операция умножения: *

Операция умножения обозначается знаком *. При выполнении оператора

z = 3 * x значение переменной x умножается на 3, и результат присваивается переменной z.

Операция деления: /

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

l = 126.0 / 2.0;

переменной l будет присвоено значение 63.0. Над данными целого типа операция деления производится не так, как над данными с плавающей точкой: в первом случае результат будет целым числом, а во втором - числом с плавающей точкой. В языке Си принято правило, согласно которому дробная часть у результата деления целых чисел отбрасывается. Это действие называется "усечением".

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

main( ) { printf("деление целых: 5/4 это %d \n", 5/4);

printf("деление целых: 6/3 это %d \n", 6/3);

printf("деление целых: 7/4 это %d \n", 7/4);

printf("деление чисел с плавающей точкой: 7./4. это %2.2f \n", 7./4.);

printf("смешанное деление: 7./4 это %2.2f \n", 7./4);

} Последний пример на использование смешанных типов, т.е. деление вещественного числа на целое. Язык Си менее строго подходит к подобным вопросам, и позволяет выполнять такие операции.

! Смешения типов следует избегать!

Результат выполнения указанной программы:

деление целых: 5/4 это 1

деление целых: 6/3 это 2

деление целых: 7/4 это 1

деление чисел с плавающей точкой: 7./4. это 1.75

смешанное деление: 7./4 это 1.75

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

Для того чтобы понять, что происходит в тех случаях, когда в одном операторе используется несколько операций, рассмотрим порядок выполнения операций. Совершенно очевидно, что изменение порядка выполнения действий может приводить к различным результатам. В языке Си каждой операции назначается уровень старшинства. Умножение и деление имеют более высокий уровень, чем сложение и вычитание, поэтому они выполняются первыми. Если же две операции имеют один и тот же уровень старшинства, они выполняются в том порядке, в котором присутствуют в операторе. Для большинства операций обычный порядок - слева направо. Операция = является исключением из этого правила. Многие программисты предпочитают представлять порядок вычислений с помощью дерева выражений. Например, выражение

(a+b)*c

будет выглядеть так:

* / \

+ c / \

a b

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

Таблица 1.1. Операции в порядке уменьшения уровня старшинства Операции Порядок вычисления ( ) слева направо - (унарный) слева направо * / слева направо + - (вычитание) слева направо = справа налево

Дополнительные операции

В языке Си имеется около сорока операций. Те операции, которые мы рассмотрели, являются наиболее общеупотребительными. Рассмотрим еще три операции, наиболее используемые программистами.

Операция деления по модулю: %

Эта операция используется в целочисленной арифметике. Ее результатом является остаток от деления целого числа, стоящего слева от знака операции, на число, расположенное справа от нее. Например, 63%5, читается как 63 по модулю 5, имеет значение 3, т.к. 12*5+3.

В результате выполнения оператора

minutes=time%60;

переменной minutes будет присвоено значение остатка от деления time на 60.

Операция увеличения: ++

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

В примере,

j=i++; переменной j сначала присваивается значение i, затем значение переменной i увеличивается на 1.

Операция уменьшения: --

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

В соответствии с принятым в языке Си порядком вычислений операции увеличения и уменьшения имеют очень высокий уровень старшинства. Только круглые скобки обладают более высоким приоритетом. Поэтому выражение a/b++ означает (a)/(b++), а не (a/b)++.

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

Перечень операций языка Си

Рассмотрим множество операций языка Си. Описанные операции входят в это множество. Язык Си отличается их большим разнообразием. В этом разделе будет обсуждаться их семантика. Каждая операция характеризуется уровнем приоритета и порядком, в котором эти операции выполняются - слева направо или справа налево. Если все операции выражения имеют один и тот же уровень приоритета, то значение выражения вычисляется слева направо в соответствии с порядком выполнения операций. Все операции с одним и тем же уровнем приоритета имеют один и тот же порядок. Однако если в выражении имеются операции с различными уровнями приоритета, то сначала выполняются операции с наивысшим уровнем приоритета, затем - следующего за ним приоритета и так далее в порядке убывания приоритета. Операции одного уровня приоритета выполняются в последовательности, указанной их порядком.

Операции, уровень приоритета которых равен 1

Операции вызова функции, индексирования и выбора.

В качестве операций языка Си рассматриваются также скобки в вызове функции, квадратные скобки для индексирования массивов, точка и стрелка вправо для выбора компонентов структуры или объединения. Уровень этих операторов равен 1, все операции выполняются слева направо.

Операция вызова функции: ( )

Пример 1:

fe(e1, e2,...,en);

Вызов функции fe с аргументами e1, e2, ..., en. Значением этого выражения является значение, которое возвращает функция.

Пример 2:

x = sqrt (y); Операция индексирования массива: []

Синтаксис:

array [2] Значением выражения является третий элемент массива.

Присвоение значения 26 одиннадцатому элементу массива записывается таким образом:

array[10]=26;

Первый элемент массива описывается выражением array[0] (более подробно о массивах описано в лекции 12).

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

Синтаксис:

struct.element

Значением этого выражения является элемент element структуры struct или объединения (см. лекцию 14). Оператор:

struct.element=1963;

присваивает значение 1963 этому элементу.

Операция выбора компонентов структуры с указателем: ->

Синтаксис:

my_birthday->day,

my_birthday - указатель на структуру.

Оператор:

my_birthday->day=26;

присваивает значение 26 структурной переменной day, на которую указывает my_birthday.

Операции, уровень приоритета которых равен 2

Унарные операции.

Для унарных операций требуется только один операнд; эти операции либо префиксные, либо префиксные и постфиксные. Операция sizeof имеет два варианта: префиксная операция и унарная операция.

Операция косвенной ссылки: *

Это указатель на любой тип T, кроме void. Тип результата T.

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

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

Пример 1:

*ptr=c; Пример 2:

*fpe; Значением выражения является функция, адресуемая указателем fpe.

Пример 3: fpe=*funcname;

(*fpe)(arg1, arg2);

Операция получения адреса: &

У этой операции тип операнда есть переменная любого типа, кроме void. Тип результата - указатель на Т.

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

&v Значением выражения является адрес переменной v.

Пример: myptr=&n;

Операция отрицания: -

Тип операнда - арифметический. Тип результата: unsigned, long, double, int.

Операция логического отрицания: !

Тип операнда - арифметический или указатель. Тип результата - int. Если операнд равен 0, то результат равен 1 и наоборот.

Пример: if(!good) printf("not good");

Операция дополнения до 1: ~

Тип операнда - интегральный. Тип результата: int, long, unsigned.

Пример:

opposite=~mask; Дополнение до единицы значения mask. Результат присваивается переменной opposite.

Операция увеличения: ++

Тип операнда - арифметический или указатель. Тип результата: int, unsigned, long, double, указатель. Значение операнда увеличивается, и становится новым значением операнда. Значение указателя увеличивается на величину указываемого объекта, значения других операндов увеличиваются на единицу.

Операция увеличения, постфиксная: ++

Тип операнда - арифметический или указатель. Тип результата: int, unsigned, long, double, указатель. Значение операнда увеличивается, но возвращается старое значение операнда. Значение указателя увеличивается на величину указываемого объекта, другие операнды увеличиваются на единицу.

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

iv++ Увеличение iv на 1. Значением этого выражения является значение iv до увеличения.

Пример:

j=i++; Использование:

pv++; Увеличение указателя pv на 1, так что он будет указывать на следующий объект того же типа. Значением этого выражения является значение pv до увеличения.

Пример: *ptr++=0;

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

Операция уменьшения: --

Тип операнда - те же, что и для ++. Тип результата - те же, что и для ++.

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

--iv Уменьшение iv на 1. Значением этого выражения является значение iv после уменьшения.

Пример:

i=--j; Операция уменьшения (постфиксная): --

Тип операнда - те же, что и для ++ (постфиксная).

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

iv--

Уменьшение iv на 1. Значением этого выражения является значение iv до уменьшения.

Пример: j=i--;

Операция определения требуемой памяти в байтах: sizeof

Тип операнда - значение любого типа или имени типа. Тип результата - unsigned. Используется как sizeof (выражение) или sizeof (имя типа).

Пример:

n=sizeof(arname)/sizeof(int);

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

Операции, уровень приоритета которых равен 3

Мультипликативные операции.

Порядок выполнения мультипликативных операций - слева направо.

Операция умножения: *

Тип операндов - арифметический. Тип результатов: int, unsigned, long, double.

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

ae1*ae2 Произведение значений ae1*ae2.

Пример: z=35*5;

Операция деления: /

Тип операндов - арифметический. Тип результатов: int, unsigned, long, double.

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

ae1/ae2 Частное от деления ae1 на ae2.

Пример: i=j/7;

Операция получения остатка: %

Тип операндов - интегральный. Тип результата int, unsigned, long. Знак остатка машинно-зависим.

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

ae1%ae2 Остаток от деления по модулю.

Пример: j=i%2;

Если i четное число, то j равно нулю.

Операции, уровень приоритета которых равен 4

Уровень приоритета аддитивных операций равен 4. Выполняются эти операции слева направо.

Операция сложения: +

Тип операндов:

арифметический;

один операнд указатель, другой - интегрального типа; оба операнда не могут быть указателями.

Тип результата: int, unsigned, long, double, указатель. Перед сложением значение целого операнда умножается на величину элемента данных, тип которых соответствует типу указателя.

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

ae1+ae2

Сумма значений ae1 и ae2.

Пример 1: i=i+100;

Первоначальное значение i увеличивает на 100.

Пример 2:

last=arname+arsize-1;

Присваивает переменной last адрес последнего элемента массива arname.

Операция вычитания: -

Тип операндов:

1. Арифметический;

2. Один операнд - указатель, другой - интегрального типа;

3. Операнды - указатели одного типа.

В первом случае тип результата: int, unsigned, long, double. Во втором случае тип результата - указатель. До вычитания значение целого операнда умножается на величину элемента данных, тип которых соответствует типу указателя. В третьем случае тип результата - int. Результат - число объектов, отделенных двумя указателями.

Арифметические действия с указателями.

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

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

ae1-ae2 Разность значений ae1 и ae2.

Пример 1:

i=j-100; Использование:

pe-ie Адрес переменной типа pe, меньше на ie адреса, заданного указателем pe.

Пример 2: first=last-arsize+1;

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

pe1-pe2 Число переменных типа pe в диапазоне от pe2 до pe1.

Пример 3: arsize=last-first;

Операции, уровень приоритета которых равен 5

Уровень приоритета операций сдвига равен 5. Порядок выполнения операций - слева направо.

Операция сдвига влево: >

Тип операнда - интегральный. Тип результата - такой же, как у левого операнда. Правый операнд преобразуется к типу int. Левый операнд сдвигается на число разрядов, равное значению правого операнда. Сдвиг будет логическим сдвигом, если левый операнд имеет тип unsigned.

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

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

Пример:

x=x>>3; Операции, уровень приоритета которых равно 6

Уровень приоритета операций отношения равен 6. Выполняются эти операции слева направо.

Операция меньше:

Тип операндов - арифметический или указатель. Тип результата - int.

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

ae1>ae2 Истина, если ae1 больше, чем ae2.

Пример: if(x>0) printf("positive");

Операция меньше или равно: =

Тип операндов - арифметический или указатель. Тип результата - int.

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

ae1>=ae Истина, если ae1 больше или равно ae2.

Операции, уровень приоритета которых равен 7

Уровень приоритетов операций равенства/неравенства равен 7, выполняются они слева направо.

Операция равенство: ==

Тип операндов - арифметический или указатель. Тип результата - int.

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

ie1==ie2 Истина, если ie1 равно ie2, иначе - ложь.

Операция неравенство: !=

Тип операндов - арифметический или указатель. Тип результата - int.

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

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

ie1!=ie2

Истина, если ie1 не равно ie2.

Пример: while (i!=0)

i=func; Операция, уровень приоритета которой равен 8

Уровень приоритета операции поразрядное И равен 8. Выполняются такие операции слева направо.

Операция поразрядное И: &

Тип операндов - интегральный. Тип результата: int, long, unsigned.

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

ie1 & ie2 Побитовая операция И двоичных представлений ie1 и ie2. Значение выражения содержит 1 во всех разрядах, в которых и ie1 и ie2 содержат 1, и 0 во всех остальных разрядах.

Пример:

flag=((x&mask)!=0);

Операция, уровень приоритета которой равен 9

Уровень приоритета операции поразрядное исключающее ИЛИ равен 9. Порядок выполнения таких операций слева направо.

Операция поразрядное исключающее или: ^

Тип операндов - интегральный. Тип результата: int, long, unsigned.

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

ie1^ie2 Побитовая операция исключающее ИЛИ двоичных представлений ie1 и ie2. Значение выражения содержит 1 в тех разрядах, в которых и ie1 и ie2 имеют разные двоичные значения, и 0 во всех остальных разрядах.

Пример:

diffbits=x^y; Операция, уровень приоритета которой равен 10

Уровень приоритета операции поразрядное включающее ИЛИ равен 10, порядок выполнения таких операторов - слева направо.

Операция поразрядное включающее ИЛИ: |

Тип операндов - интегральный. Тип результата: int, long, unsigned.

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

ie1 | ie2 Побитовая операция ИЛИ двоичных представлений ie1 и ie2. Значение выражения содержит 1 во всех разрядах, в которых ie1 или ie2 содержат 1, и 0 во всех остальных разрядах.

Пример: attrsum=attr1 | attr2;

Операция, уровень приоритета которой равен 11

Уровень приоритета логической (условной) операции И равен 11. Выполняется операция слева направо.

Операция логическое И: &&

Тип операндов - арифметический или указатель. Тип результата - int. Если первый операнд равен 0, то результат равен 0. В противном случае результат будет равен 1, если второй операнд не равен 0, и равен 0, если второй операнд равен 0 (если первый операнд равен 0, то второй операнд не вычисляется).

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

e1&&e2 Логическая операция И значений e1 и e2. Вначале проверяется значение e1; значение e2 проверяется только в том случае, если значение e1 -Истина. Значением выражения является Истина, если значения e1 и e2 - Истина.

Пример:

if(p!=NULL&&*p>7) n++;

Если p - не нулевой указатель и значение переменной, на которую указывает p, больше, чем 7, то в этом случае n увеличивается на 1. Обратите внимание, что если значение указателя p равно NULL(0), то выражение *p не имеет смысла.

Операция, уровень приоритета которой равен 12

Уровень приоритета операции равен 12, выполняются такие операции слева направо.

Операция логическое ИЛИ: ||

Тип операндов - арифметический или указатель. Тип результата int. Если хотя бы один операнд не равен 0, результат равен 1; иначе результат будет равен 0.

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

e1 || e2 Логическая операция ИЛИ значений e1 и e2. Вначале проверяется значение e1; значение e2 проверяется только в том случае, если значение e1 - ложь. Значением выражения является Истина, если истинно любое значение e1 или e2.

Пример 1:

if(xB) printf("out of range");

Операция, уровень приоритета которой равен 13

Уровень приоритета операции равен 13, выполняются такие операции слева направо.

Операция условный оператор: ?

Тип операндов - арифметические; второй и третий операнды могут быть указателями, структурами, объединениями. Тип результата: int, long, unsigned, double, указатель, структура или объединение. Второй и третий операнды преобразуются к одному и тому же типу.

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

a?b:c где a, b, c - выражения. Если a не равно 0, то результат выражения a ? b : c равен b; иначе результат равен c. Из двух последних операндов вычисляется только один.

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

ae?e1:e2 или

pe?e1:e2 (где pe - указатель)

Если истинно ae или pe, то выполняется e1; иначе выполняется e2. Значением этого выражения является значение выражения e1 или e2.

Пример:

abs=(i один из знаков : +, -, *, /, %, >>, = e

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

v = v e Пример 1:

y+=2; /* Увеличение переменной y на 2 */

p+=n; x-=3; ptr-=2;

timesx*=x; x/=2;

x%=10; x>>=4; x= e

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

v = v e

этот операнд вычисляется дважды. Это различие проявляется в побочных эффектах, связанных с вычислением операнда v, например, при изменении значения какой-либо переменной. Рассмотрим это на примерах:

a[i++]* = n; При выполнении вычисление левого операнда дает побочный эффект - увеличение значения переменной i. Следовательно, это присваивание не эквивалентно присваиванию

a[i++] = a[i++]*n;.

Эквивалентом первой операции присваивания может служить последовательность операций

a[i]=a[i]*n;

i=i+1; а эквивалентом второй - последовательность операций

a[i]=a[i+1]*n; i=i+2;

или последовательность операций

a[i+1]=a[i]*n;

i=i+2; в зависимости от того, какая часть операции присваивания вычисляется раньше - левая или правая. Порядок таких вычислений не определен.

Операция, уровень приоритета которой равен 15

Уровень приоритета операции равен 15, выполняются такие операции слева направо.

Операция запятая: ,

Тип результата совпадает с типом правого операнда. Операция объединяет два выражения в одно выражение, значением которого является значение правого операнда; значение левого операнда вычисляется только для получения побочных эффектов.

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

el,e2 Сначала выполняется выражение e1, потом выражение е2. Значением всего выражения является значение выражения е2.

Пример:

for(i=A,j=B;i #include

/* подключение библиотеки математических функций */

const float pi=3.14159;

float r=10; float l=50; float s;

main() { s=pi*r*l;

printf("\n Площадь боковой поверхности s=%f",s);

} /* Задача № 2 (пример на использование операций +,

* и /). Тело движется прямолинейно с ускорением.

Даны а(м/сек2) - ускорение, V(M/C) - начальная

скорость. Требуется определить, какой путь пройдет

тело за t секунд.*/

#include

float а, v, t, s ;

main() { a=20.2;

v=50.3; t=65;

s=(v*t)+(a*t*t/2);

printf("\n Путь s=%f M",S);

} /* Задача № 3 (пример на использование операций =,

/ и вычисления степени). Вычислите среднее арифметическое

и среднее геометрическое трех чисел а,b,с.*/

#include #include

float a=b=c=2005.1; float x,у;

main () { x=(a+b+c)/3;

y=pow( (a*b*c),(1/3));

/*функция pow берется из библиотеки math.h*/

ntf("\n среднее арифметическое x=%f",x);

ntf("\n среднее геометрическое y=%f",y);

} /* Задача № 4 (пример на использование операций

=,*, / и функции hypot). Даны катеты прямоугольного

треугольника. Найти его гипотенузу и площадь.*/

#include #include

float x=y=50.7; float z,s;

main() { z=hypot(х, у) ;/* вычисление гипотенузы*/

s=x*y/2; ntf("\n гипотенуза=%f см",z);

printf("\n Плoщaдь=%f кв см",s);

}

4. Лекция: Операторы.

-Выражения.

-Простейшие выражения.

-Операторы.

-Составные операторы.

Выражения

Выражение представляет собой объединение операций и операндов. Напомним, что операндом называется то, над чем выполняется операция. Простейшее выражение состоит из одного операнда. Опираясь на это понятие выражения, мы можем строить более сложные конструкции. Приведем несколько выражений:

100 1904 +100

a*(c-d) x=0 x=y++

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

В отличие от большинства других языков, в языке Си для задания определенного порядка вычисления выражения недостаточно только соответствующей расстановки скобок, так как компилятор может произвольно переупорядочивать выражения, включающие ассоциативные и коммутативные операторы (*,+,|,^) даже при наличии скобок. Для задания желаемого порядка выполнения выражения нужно использовать дополнительные присваивания, если требуется, с использованием временных переменных.

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

j=3;

i=(k=j+1)+(j=5);

значение переменной i будет равно 9 или 11 в зависимости от того, какое подвыражение второй операции будет вычислено первым. Таким образом, с использованием разных компиляторов можно получить различные результаты.

Простейшие выражения

Простейшими выражениями называются выражения, сформированные с использованием констант типов int, char и enum, операции sizeof, унарных операторов - и ~, бинарных операторов + ~ * / % & | ^ > = = != = и тернарной операции ?:.

Простейшие выражения используются в операторе switch, в инициализаторах границ массивов и в операторе препроцессора #if.

Логические операции ||, && являются условными логическими операциями, т. к. второй операнд вычисляется только при необходимости. В других языках программирования, например, в языке Паскаль, в логических операторах всегда вычисляются значения обоих операндов, даже если результат может быть определен вычислением одного операнда.

Важным свойством языка Си является то, что каждое выражение в Си имеет значение. Приведем несколько выражений и их значения:

Выражение Значение -14+16 2 a=3+8 11 5>3 1 14

#include float km=10;

int d; main() {

clrscr(); /* чистка экрана, функция берется

из головного файла conio.h*/

d=1; /* первый день, за который велосипедист

проехал 10 км.*/

while(km float р,t,s,d;

main()

{ puts("Введите сумму s");

scanf("%f",&s);

puts("Введите процентную ставку р");

/* вывод строки */

scanf("%f",&p);

puts("Введите время хранения в днях t");

scanf("%f",&t);

d=p*s*t/36500;

printf("\n Величина дохода d=%f",d);

} /* Задача № 2

По заданной стороне куба А определить его объем, площадь

грани и площадь поверхности.*/

#include float a,v,s,si;

main() { puts("\n Введите сторону а");

scanf("%f",&a);

v=a*a*a;

s=a*a; si=s*6;

printf("\n объем v=%f",v);

printf("\n площадь грани s=%f",s);

printf("\n площадь поверхности si=%f",si);

} /* Задача № 3

Определить реальное расстояние между населенными

пунктами. Указан масштаб карты и расстояние между

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

#include float m,s,si;

main() { puts("Введите масштаб карты m");

scanf("%f",&m);

puts("Введите расстояние на карте si");

scanf("%f", &si);

s=si*m; printf("\n реальное расстояние s=%f",s);

} 5. Лекция: преобразование типов.

-Эквивалентность типов.

-Преобразование типов.

-Неявное преобразование типа.

-Арифметические преобразования.

-Явное преобразование типов.

-Синтаксис типов.

Эквивалентность типов

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

Большинство реализаций языка Си используют схему структурной эквивалентности типов. Однако в книге (Ritche, D.M. 1980/ The C Programming Language - Reference Manual/ AT&T Bell Laboratories, Murray Hill, N.J. 07974) вопрос об эквивалентности типов игнорируется, и при каждой реализации может быть выбрана своя схема определения эквивалентности типов. Следовательно, вполне возможно, что результаты правильно работающей программы станут неверными при замене компилятора!

Преобразование типов

В операторах и выражениях должны использоваться переменные и константы только одного типа. Если все же мы смешиваем типы в одном выражении, то компилятор с языка Си не считает программу неправильной, как это произошло бы при программировании на языке Паскаль. Вместо этого компилятор использует набор правил для автоматического преобразования типов. Это очень удобно, но может оказаться и опасным, особенно если мы допустили смешение типов нечаянно.

Приведем несколько основных правил, касающихся преобразования типов:

1. Если операция выполняется над данными двух различных типов, обе величины приводятся к высшему из двух типов. Этот процесс называется повышением типа.

2. Последовательность имен типов, упорядоченных от высшего типа к низшему, выглядит так: double, float, long, int, short, char. Применение ключевого слова unsigned повышает ранг соответствующего типа данных со знаком.

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

Повышение типа обычно происходит гладко, в то время как понижение может привести к затруднениям. Причина этого проста: все число целиком может не поместиться в элементе данных низшего типа. Переменная типа char может иметь целое значение 101, но не 22225.

Пример, приведенный ниже, иллюстрирует применение этих правил:

/*преобразования*/

main( ) { char ch;

int i; float f1;

f1=i=ch='A'; /***8***/

printf("ch=%c,i=%d,f1=%2.2f\n",ch,i,f1); /***9***/

ch=ch+1; /***10***/

i=f1=f1+2*ch; /***11***/

f1=2.0*ch+i; /***12***/

printf("ch=%c,i=%d,f1=%2.2f\n",ch,i,f1); /***13***/

ch=2.0e30;/***14***/

printf("Теперь ch=%c\n",ch);

} Выполнив программу "преобразования", получим следующие результаты:

ch = A, i = 65, f1 = 65.00

ch = B, i = 197, f1= 329.00

Теперь ch =

Разбор программы

Строки 8 и 9: величина 'A' присваивается символьной переменной ch. Переменная i получает целое значение, являющееся преобразованием символа 'A' в целое число, т.е. '65'. Переменная f1 получает значение 65.00, являющееся преобразованием числа 65 в число с плавающей точкой.

Строки 10 и 13: значение символьной переменной 'A' преобразуется в целое число 65, к которому затем добавляется 1. После этого получившееся в результате число 66 преобразуется в код символа В и помещается в переменную ch.

Строки 11 и 13: при умножении на 2 значение переменной ch преобразуется в целое число 66. При сложении с величиной переменной f1 получившееся в результате число 132 преобразуется в число с плавающей точкой. Результат 197.00 преобразуется в число целого типа и присваивается переменной i.

Строки 12 и 13: перед умножением на 2.0 значение переменной ch('B') преобразуется в число с плавающей точкой. Перед выполнением сложения величина переменной i(197) преобразуется в число с плавающей точкой, а результат 329.00 присваивается переменной f1.

Строки 14 и 15: здесь производится попытка осуществить преобразование типов в порядке убывания старшинства - переменная ch полагается равной сравнительно большому числу. Результаты оказываются неутешительными. Независимо от переполнения и усечения, которые имеют место, в итоге мы получили код, соответствующий какому-то непечатаемому знаку.

Существует еще один вид преобразования типов. Для сохранения точности вычислений при арифметических операциях все величины типа float преобразуются в данные типа double. Это существенно уменьшает ошибку округления. Конечный результат преобразуется обратно в число типа float, если это диктуется соответствующим оператором описания.

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

Самое лучшее - это вообще избегать преобразования типов, особенно в порядке убывания ранга. Но иногда оказывается удобным применять такие преобразования при условии, что мы ясно представляем смысл выполняемых действий. Преобразования типов, которые мы обсуждали до сих пор, выполнялись автоматически. Существует возможность точно указывать тип данных, к которому необходимо привести величину. Этот способ называется приведением типов, и используется следующим образом: перед данной величиной в круглых скобках записывается имя требуемого типа. Скобки и имя типа вместе образуют операцию приведения. В общем виде она записывается так:

(тип) где фактическое имя требуемого типа представляется вместо слова тип.

Рассмотрим пример:

int nice;

nice = 1.6+1.7; nice = (int)1.6+(int)1.7;

В первом примере используется автоматическое преобразование типов. Сначала числа 1.6 и 1.7 складываются - результат равен 3.3. Затем путем отбрасывания дробной части полученное число преобразуется в 3 для согласования с типом int переменной nice. Во втором примере 1.6 и 1.7 преобразуются в целые числа 1, так что переменной nice присваивается значение, равное 1+1, или 2.

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

Неявное преобразование типа

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

char - int, short int, long int (Преобразование к значению с большим числом двоичных разрядов может включать, а может не включать расширение знакового разряда - это зависит от реализации языка. Для элементов заданного набора знаков гарантируется преобразование в неотрицательные целые значения).

int - char, short int, long int (Преобразование к целому большей длины включает расширение знакового разряда. Преобразование к целому меньшей длины вызывает отбрасывание лишних старших разрядов). float, double, unsigned int (интерпретация комбинации битов в виде беззнакового целого значения).

short int - аналогично типу int.

long int - аналогично типу int.

float - double, int, short int, long int (машинно-зависимое преобразование, если преобразуемое значение слишком велико, то результат неопределен).

double - float (преобразование с округлением и последующим отбрасыванием лишних разрядов), int, short int, long int.

Арифметические преобразования

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

Преобразовать операнды типов char и short int к типу int; преобразовать операнды типа float к типу double.

Если хотя бы один из операндов имеет тип double, то и другой операнд преобразуется к типу double (если он другого типа); результат имеет тип double.

Если хотя бы один операнд имеет тип long, то и другой операнд преобразуется к типу long (если он другого типа); результат имеет тип long.

Если хотя бы один из операндов имеет тип unsigned, то и другой операнд преобразуется к типу unsigned (если его тип не unsigned); результат имеет тип unsigned.

Если ни один из случаев 1-4 не имеет места, то оба операнда должны иметь тип int; такой же тип будет и у результата.

Явные преобразования типов

Выражения могут быть преобразованы из одного типа в другой явным указанием. Выражение E может быть явно преобразовано к типу имя-типа с помощью записи вида

(имя - типа) Е

где имя типа представляется в форме

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

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

Т абстрактный описатель

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

форма абстрактного описателя - смысл слов "Т абстрактный описатель";

пустой (абстрактный описатель) - абстрактный описатель типа Т;

*(абстрактный описатель) - указатель на тип Т;

абстрактный описатель ( ) - функция, возвращающая значение типа Т;

абстрактный описатель [n] - массив с n элементами типа Т, n - выражение с постоянным значением;

Приведем примеры явного преобразования. Предположим, что даны следующие определения и описания:

int i;

char *pc, *name;

char *calloc( ), *strcpy( );

тогда можно привести следующие примеры явных преобразований типов:

(char) i - преобразует значение типа int в значение типа char.

pc=(char *) 0777 - преобразует восьмеричный литер 0777 в значение указателя на знак таким образом, что оно может быть присвоено переменной "pc".

(emp *) calloc(1,sizeof(emp)) - преобразует значение "знакового" указателя, возвращаемого функцией calloc, в значение указателя emp.

(void) strcpy(name,"gehani") - опускает значение, возвращенное функцией strcpy.

Синтаксис типов

Можно отметить, что синтаксис типов в языке Си нерегулярен и беспорядочен, о чем свидетельствуют:

* - Трудность создания форматеров.

* - Большое число ошибок, допускаемых даже опытными программистами.

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

Отсутствие формального описания синтаксиса языка Си - даже книга (Kernighan, B.W., and D.M. Ritchie, The C Programming Language, prentice-Hall, Engleewood Cliffs, NJ (1978)) может служить лишь черновым описанием такового. Сказанное касается в полной мере и синтаксиса типов. Имеется три случая, когда необходимо использовать типы:

1. Описания, связывающие тип с именем, как, например, в случае описания (глобальных) переменных и формальных параметров;

2. Описания, связывающие значения (и тип) с именем, как, например, описания программ;

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

6. Лекция: функции и переключение ввода-вывода

-Ввод и вывод одного символа.

-Буферы. -Чтение одной строки.

-Чтение файла.

-Переключение и работа с файлами.

Ввод и вывод одного символа

В данном разделе мы рассмотрим функции, применяемые при вводе и выводе. Кроме того, мы коснемся других аспектов этого понятия. Под функциями ввода-вывода подразумеваются функции, которые выполняют транспортировку данных в программу и из нее. Мы уже использовали две такие функции: printf( ) и scanf( ). Теперь рассмотрим несколько других возможностей, предоставляемых языком Си.

Функции ввода-вывода не входят в определение языка Си. Их разработка возложена на программистов, реализующих компилятор с языка Си. С другой стороны, выгода использования стандартного набора функций ввода-вывода на всех системах очевидна. Это дает возможность писать переносимые программы, которые легко можно применять на разных машинах. В языке Си имеется много функций ввода-вывода такого типа, например printf( ) и scanf( ). Ниже мы рассмотрим функции getchar( ) и putchar( ).

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

Функция getchar( ) получает один символ, поступающий с пульта терминала (и поэтому имеющий название), и передает его выполняющейся в данный момент программе. Функция putchar( ) получает один символ, поступающий из программы, и пересылает его для вывода на экран. Рассмотрим пример программы, которая принимает один символ с клавиатуры, и выводит его на экран:

/*ввод-вывод*/

#include main( )

{ char ch; ch=getchar( ); /***1***/

putchar(ch); /***2***/

} Для большинства систем спецификации функции getchar и putchar содержатся в системном файле stdio.h, поэтому мы указали данный файл в программе. Функция getchar( ) аргументов не имеет, т.е. при ее вызове в круглые скобки не помещается никакая величина. Она просто получает очередной поступающий символ, и сама возвращает его значение выполняемой программе. Оператор, приведенный в строке 1, присваивает значение функции getchar( ) переменной ch. Функция putchar( ) имеет один аргумент. При ее вызове необходимо в скобках указать символ, который требуется вывести на печать. Аргументом может быть одиночный символ (включая знаки, представляемые управляющими последовательностями), переменная или функция, значением которой является одиночный символ. Правильным обращением к функции putchar( ) является указание любого из этих аргументов при ее вызове:

putchar('D');

putchar('\n'); putchar('\007');

putchar(ch); /* переменная типа char */

putchar(getchar( ));

Модифицируем нашу программу:

#include main( )

{ putchar(getchar( ));

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

Буферы

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

Зачем нужны буферы?

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

Рассмотрим вывод на печать групп символов. Желательно, чтобы в любой момент можно было остановить работу программы. Для этого напишем программу так, чтобы она прекращала работу при получении какого-нибудь специального символа, например "!":

/* ввод-вывод */

/* ввод и печать символов до поступления

завершающего символа */

#include #define STOP '!' /*дает символу '!' символическое имя */

main( )

{ char ch; ch=getchar( ); /***9***/

while(ch != STOP) { /***10***/

putchar( ch); /***11***/

ch=getchar( ); /***12***/

} } В данном примере при первом прохождении тела цикла функция putchar( ) получает значение своего аргумента в результате выполнения оператора, расположенного в строке 9. В дальнейшем, вплоть до завершения работы цикла, значением этого аргумента является символ, передаваемый программе функцией getchar, расположенной в строке 12. Цикл while будет осуществлять чтение и печать символов до тех пор, пока не поступит признак STOP.

! Программа, приведенная ниже, делает то же самое, но стиль ее написания лучше отвечает духу языка Си:

/* ввод-вывод */

#include

#define STOP '!'

main( ) { char ch;

while ((ch=getchar( )) != STOP) /***8***/

putchar(ch);

} Одна строка 8 этой программы заменяет строки 9, 10, 12 предыдущей программы.

Чтение одной строки

Усложним пример ввода-вывода:

/* подсчет символов */

#include #define STOP '!'

main( ) { char ch;

/*инициализация счетчика символов 0 */

int count = 0;

while ((ch=getchar( )) != STOP) {

putchar(ch); count++; /* прибавить 1 к счетчику */

} printf("\n Всего было прочитано %d символа.\n",

count);

} Если мы хотим просто подсчитать число введенных символов без отображения их на экране, функцию putchar( ) можно опустить.

Заменим признак окончания ввода данных, используем символ новая строка \n. Для этого нужно переопределить признак STOP:

#define STOP '\n'

Символ новая строка пересылается при нажатии клавиши Enter. Предположим, что мы внесли указанное изменение в программу "подсчет символов", а затем при выполнении ввели следующую строку:

На экране тридцать четыре символа.[Enter]

В ответ на экране появятся следующие строки:

На экране тридцать четыре символа.

Признак, появляющийся в результате нажатия клавиши Enter, не входит в число символов 34, подсчитанных программой, поскольку подсчет осуществляется внутри цикла. Теперь у нас есть программа, которая может прочесть одну строку.

Чтение файла

Если нам нужно читать большие порции данных, например из файла, каким должен быть признак STOP? Это должен быть такой символ, который обычно не используется в тексте и, следовательно, не приводит к ситуации, когда он случайно встретится при вводе, и работа программы будет остановлена раньше, чем бы мы хотели. Файлом можно назвать участок памяти, в который помещена некоторая информация. Обычно файл хранится в некоторой долговременной памяти, например на гибких или жестких дисках или на магнитной ленте. Чтобы отмечать, где кончается один файл и начинается другой, полезно иметь специальный символ, указывающий на конец файла, чтобы отмечать конец файла и начинать другой. Это должен быть символ, который не может появиться где-то в середине файла. Решением указанной проблемы служит введение специального признака, называемого "End-of-File", конец файла, или EOF. Выбор конкретного признака EOF зависит от типа системы. Он может состоять даже из нескольких символов. Обычно определение EOF содержится в файле . Общеупотребительным является определение

#define EOF (-1)

Пример: /* ввод-вывод_ф */

#include

main( ) { int ch;

while ((ch = getchar( )) != EOF)

putchar(ch); }

Это надо помнить:

1. Не нужно самим определять признак EOF. Он описан в файле .

2. Мы можем не интересоваться фактическим значением символа EOF, поскольку директива #define, имеющаяся в файле , позволяет нам использовать его символическое представление.

Мы изменили в нашей программе тип переменной ch с char на int. Это мы сделали, потому что значением переменных типа char является целое без знака в диапазоне от 0 до 255, а признак EOF может иметь числовое значение -1. Эта величина недопустима для переменной типа char. Функция getchar( ) фактически возвращает значение типа int, поэтому она в состоянии прочесть символ EOF.

Переменная ch целого типа никак не может повлиять на работу функции putchar( ). Она просто выводит на печать символьный эквивалент значения аргумента.

При работе с данной программой, когда символы вводятся с клавиатуры, необходимо уметь вводить признак EOF. В большинстве реализаций операционной системы UNIX, например, ввод [CTRL/d] (нажать на клавишу [d], держа нажатой клавишу [CTRL]) интерпретируется как признак EOF. Во многих микрокомпьютерах для той же цели используется знак [CTRL/z].

Пусть мы ввели фразу с клавиатуры. Приведем результат работы программы "ввод-вывод_ф" в системе, с буферизованным вводом:

Спрос на высокопрофессиональных ИТ-специалистов

Спрос на высокопрофессиональных ИТ-специалистов

растет как со стороны государственных, так и

частных компаний растет как со стороны

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

[CTRL/z] Каждый раз при нажатии клавиши Enter производится обработка символов, попавших в буфер, и копия строки выводится на печать. Это продолжается до тех пор, пока мы не введем признак EOF. Программа "ввод-вывод_ф" осуществляет вывод на экран символов независимо от того, откуда они поступают. Наша программа могла бы просматривать содержимое файлов, создавать новые файлы и получать копии файлов. Решение этих проблем - в управлении вводом и выводом.

Переключение и работа с файлами

Понятие ввода-вывода включает в себя функции, данные и устройства. Рассмотрим, например, программу "ввод-вывод_ф". В ней используется функция getchar( ), осуществляющая ввод, причем устройство ввода - клавиатура (в соответствии с нашими предположениями), а выходные данные - отдельные символы. Изменим источник поступления в программу данных. По умолчанию Си-программа рассматривает стандартный ввод как источник поступления данных. Стандартным вводом называется устройство, принятое в качестве обычного средства ввода данных в машину. Это может быть устройство чтения данных с магнитной ленты телетайпа или терминал. Мы можем сами выбирать устройство данных из любого источника. Ну, например, мы можем написать в программе, что источник входных данных - файл, а не клавиатура.

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

Первый способ заключается в явном использовании специальных функций, которые открывают и закрывают файлы, организуют чтение и запись данных и т.д. Этот вопрос мы будем обсуждать в 15 лекции.

Второй способ состоит в том, чтобы использовать программу, спроектированную первоначально в предположении, что данные в нее вводятся с клавиатуры и выводятся на экран, но переключить ввод и вывод на другие информационные каналы, например, из файла в файл. Этот способ в некоторых отношениях обладает меньшими возможностями, чем первый, но зато гораздо проще в использовании. Операция переключения - это средство OC UNIX, а не самого языка Си. Но она оказалась настолько полезной, что при переносе компилятора с языка Си на другие вычислительные системы часто вместе с ним переносится и эта операция. Многие из вновь созданных операционных систем, таких, как MS-DOS 2, включают в себя данное средство. Сначала мы обсудим возможности этой операции в OC UNIX, а за тем и в других системах.

Переключение вывода.

Предположим, мы осуществили компиляцию программы "ввод-вывод_ф" и поместили выполняемый объектный код в файл с именем get_put. Затем, чтобы запустить данную программу, мы вводим с терминала только имя файла

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

get_put my_words

и начать ввод символов. Символ > служит обозначением операции переключения, используемой в OC UNIX. Ее выполнение приводит к тому, что создается новый файл с именем my_words, а затем результат работы программы "ввод-вывод_ф", представляющий собой копию вводимых символов, направляется в данный файл. Если файл с именем my_words уже существует, он обычно уничтожается, и вместо него создается новый. На экране появляются вводимые слова. Их копии будут направлены в указанный файл. Чтобы закончить работу, мы вводим EOF, в OC UNIX это обычно [CTRL/d].

Комбинированное переключение

Предположим, что мы хотим создать копию файла my_words и назвать его my_words2. Нужно ввести для этого команду

get_put my_words2

и требуемое задание будет выполнено.

Команда

get_put > my_words2 . Операция переключения отличается от аналогичной операции в двух аспектах:

Указанная операция выполняется при работе программ, написанных только на Си, в то время как в OC UNIX она может использоваться при работе любой программы.

Между именем файла с откомпилированной программой и знаком операции должен быть один пробел, а между знаком операции и именем файла пробел должен отсутствовать. Например:

get_put

main( ) { int ch;

int line_count=0;

while((ch = getchar( )) != EOF)

if(ch == '\n') line_count++;

printf("Мы насчитали %d строк. \n", line_count);

} Оператор if служит указанием компьютеру увеличить значение переменной line_count на 1, если только что прочитанный символ, содержимое переменной ch, представляет собой символ "новая строка". Что происходит в случае, когда значение переменной ch не является символом "новая строка"? Тогда в цикле while производится чтение следующего символа. Оператор if считается одиночным оператором, начинающимся от ключевого слова if и завершающимся символом "точка с запятой". Модифицируем программу. Подсчитаем одновременно число символов и строк в файле:

/* подсчет числа строк и символов */

#include

main( ) { int ch = 0;

int line_count = 0;

int char_count = 0;

while((ch = getchar( )) != EOF) {

char_count++;

if(ch == '\n')

line_count++;

}

printf("Мы насчитали %d символов и %d строк.\n",

char_count, line_count);

} Теперь в цикл while входят два оператора, поэтому мы использовали фигурные скобки, чтобы отметить начало и конец цикла.

Расширение оператора if

Простейшей формой оператора if является

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

оператор

Под выражением здесь понимаем условное выражение. С его помощью сравниваются значения двух величин. Если такое выражение истинно, то оператор выполняется. В противном случае он пропускается. В общем случае в качестве условия может быть использовано любое выражение, и если его значение равно 0, то оно считается ложным.

Оператор может быть простым или составным, т. е. блоком.

Пример:

/* пример одиночного оператора и блока */

#include main( )

{ int i; i=getchar( );

if(i > 5)

/* одиночный оператор*/

printf("Значение больше 5.\n");

if(i main( )

{ char ch;

while ((ch=getch( )) != EOF) {

/* оставить символ "новая строка неизменным" */

if (ch == '\n') putchar(ch);

else

/* заменить остальные символы*/

putchar(ch + 1);

} } Общий вид оператора if-else выглядит следующим образом:

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

оператор

else оператор

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

! Правила языка Си не требуют отступа в строке, но это стало обычной практикой. Отступ позволяет с первого взгляда заметить те операторы, выполнение которых зависит от проверяемого условия. Простая конструкция if позволяет нам выбрать, выполнить или нет некоторое действие; конструкция же if-else дает возможность выбрать одно из двух действий.

Подведем итоги.

Оператор if имеет две формы:

if(выражение) оператор;

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

оператор1;

else оператор2;

Если в результате вычисления значения выражения получено значение "истина" (ненулевое значение), то в обоих формах оператора if выполняется оператор1. Если вычисленное значение выражения равно значению "ложь" (нулевое), тогда выполнение оператора if, представленного в первой форме, заканчивается, а в операторе, имеющем вторую форму, выполняется оператор 2.

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

Например, вложенный оператор if

if(e1) if( e2) оператор1; else оператор2;

может быть интерпретирован так

if (e1) if(e2)

оператор1;

else оператор2;

или как if(e1)

if(e2) оператор1;

else

оператор2;

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

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

if(e2) оператор1;

else ; /* точка с запятой здесь обозначает пустой

оператор*/

else оператор2;

Для явного указания намерений программиста можно использовать и фигурные скобки. Например, обе вышеприведенные интерпретации можно записать явно так:

if(e1) {

if(e2) оператор1;

else оператор2

} и if(e1) { if(e2) оператор1;

} else

оператор2;

Операции отношения

Операции отношения используются для сравнений. Мы уже обсуждали их, но здесь мы остановимся на их обсуждении подробнее.

Операция смысл = больше или равно > больше != не равно Этот список хорошо соответствует возможным числовым соотношениям.

! Главное предостережение, которое мы хотим сделать, состоит в том, чтобы не использовать знак = вместо ==. С помощью операции присваивания некоторое значение присваивается переменной слева от знака равенства. В то же время с помощью операции отношения "равенство" проверяется: равно ли выражение, стоящее слева от знака, выражению справа от него. Эта операция не изменяет значения переменной в левой части, если она там присутствует. При программировании требуется аккуратность, потому что в ряде случаев компилятор не сможет обнаружить ошибки, связанные с неправильным использованием знаков этих отношений, что приведет к результатам, отличным от тех, которые вы должны были бы получить. Операции отношения применяются при формировании условных выражений, используемых в операторе if и while. Указанные операторы проверяют, истинно или ложно данное выражение.

Пример:

#include main( )

{ int mark; mark = getchar( );

if(mark == 4)

printf("Оценка 4.\n");

else if (mark > 4)

printf("Оценка больше 4!\n");

else /* случай, когда оценка меньше 4 */

{

mark++; printf("Теперь оценка на 1 больше.\n");

} } Что такое истина

Мы ответим на этот вопрос, как он решается в языке Си. В Си выражение всегда имеет значение. Это утверждение остается верным даже для условных выражений, как показывает пример, приведенный ниже. В нем определяются значения двух условных выражений, одно из которых оказывается истинным, а второе - ложным:

/* истина и ложь*/

main( ) { int true, false;

true = (12 > 2); /*отношение истинно*/

false = (12 == 2) ; /*отношение ложно*/

printf("true = %d; false = %d\n",true,false);

} При выполнении программы получим следующие результаты:

true = 1, false = 0.

В языке Си значение истина равно 1, а значение ложь равно 0. В языке Си все ненулевые значения являются истинными, и только 0 являются ложью. Например, два оператора

if(i !=0 )

можно заменить на

if(i) поскольку выражение (i !=0 ) и выражение (i) оба примут значения 0, или ложь, только в том случае, если значение переменной i равно 0.

Осложнение с понятием истина

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

/* занятость */

main( ) { int age =20;

while (age++

#include

#include main()

{ float a,b;

char opr; float result=0;

int I,i=0;

clrscr(); printf("Сколько раз будете считать?:\n");

scanf("%I",&I);

printf("Введите операнд, операцию, операнд:\n");

while(i

main( ) { int i = 1;

char res; printf("Задумайте целое число от 1 до 100.

Компьютер попытается угадать его. \n");

printf ("Отвечайте y если догадка правильна и");

printf("\n n, если программа ошибается \n");

printf("И так, ваше число %d?\n",i);

/*получение ответа */

while((res = getchar( )) !='y')

if(res !='\n')

/* пропуск символа новая строка */

printf("Ну тогда оно равно %d\n" ,++i);

printf("Число угадано!\n");

} ! Это довольно простая программа. Она написана правильно и решает поставленную задачу, но делает это крайне неэффективно. Данный пример показывает, что правильность написания - не единственный критерий, по которому необходимо оценивать программу. При этом очень важна ее эффективность! В наших примерах до сих пор использовались условные выражения, но вообще говоря, это могут быть выражения произвольного типа. В качестве оператора можно использовать простой оператор с символом "точка с запятой" в конце или составной оператор, заключенный в фигурные скобки. Если выражение истинно (или в общем случае равно единице), то оператор, входящий в цикл while, выполняется один раз, а затем выражение проверяется снова. Эта последовательность действий, состоящая из проверки и выполнения оператора, периодически повторяется до тех пор, пока выражение не станет ложным (или в общем случае равным нулю). Каждый такой шаг называется итерация. Данная структура аналогична структуре оператора if. Основное отличие заключается в том, что в операторе if проверка условия и (возможное) выполнение оператора осуществляется только один раз, а в цикле while эти действия производятся, вообще говоря, неоднократно.

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

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

Оператор while определяет операции, которые циклически выполняются до тех пор, пока проверяемое выражение не станет ложным, или равным нулю. Оператор while - это цикл с предусловием. Решение, выполнить ли в очередной раз тело цикла, принимается перед началом его прохождения. Поэтому вполне возможно, что тело цикла не будет выполнено ни разу. Оператор, образующий тело цикла, может быть либо простым, либо составным. Форма записи:

while (выражение) оператор

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

Цикл со счетчиком

Оператор цикла for

for(e1; e2; e3) s

является удобной сокращенной записью для цикла while вида

e1; while(e2) {

s; e3; }

Выражение e1 служит для задания начальных условий выполнения цикла, выражение e2 обеспечивает проверку условия выхода из цикла, а выражение e3 модифицирует условия, заданные выражением e1. Любое из выражений e1, e2, e3 может быть опущено. Если опущено e2, то по умолчанию вместо него подставляется значение TRUE. Например, цикл for

for(;e2;) s;

с опущенными e1,e3 эквивалентен циклу

while(e2) s; Цикл

for(;;) s; со всеми опущенными выражениями эквивалентен циклу

while(TRUE) s;

т.е. эквивалентен бесконечному циклу. Такой цикл может быть прерван только явным выходом из него с помощью операторов break, goto, return, содержащихся в теле цикла s.

Несмотря на внешнее сходство с итеративными циклами for языков Паскаль и Ада или итеративными циклами do языков Фортран или ПЛ/1, цикл for языка Си не является их семантической копией. Цикл for языка Си обладает большей общностью, чем циклы for и do других языков. В отличии от этих циклов, в общем случае число итераций в цикле for языка Си не может быть определено до выполнения этого цикла.

Цикл for и его альтернатива цикл while семантически почти эквивалентны, но, как указывается в работе (Ritchie, D.M. 1980. The Programming Language - Reference Manual/. AT&T Bell Laboratories, Murray Hill, N.J. 07974), не идентичны. Например, рассмотрим случай, когда оператор s является оператором continue или составным оператором, содержащим оператор continue. Действие оператора continue состоит в переходе к концу цикла, что имеет различные последствия для цикла for и его эквивалента в форме цикла while. В случае с циклом for выражение e3 выполняется до вычисления значения выражения e2, в то время как в эквивалентном цикле while выражение e3 пропускается.

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

Можно применять операцию уменьшения для счета в порядке убывания вместо счета в порядке возрастания:

for(n=10; n>0; n--)

printf("%d секунд!\n",n);

printf("Пуск! \n");

При желании можно вести счет двойками, десятками и т.д.:

for (n=2; n

/* Функция суммирует значения своих параметров */

long summa(int m,...) /*m - число параметров*/

{ int *p=&m; /*настроили указатель на параметр m*/

long t=0;

for(;m;m--) t+=*(++p);

return t; } void main()

{ printf("\n summa(2,6,4)=%d",summa(2,6,4));

printf("\n summa(6,1,2,3,4,5,6)=%d", summa(6,1,2,3,4,5,6));

}

10. Лекция: Классы памяти и разработка программ.

-Классы памяти и область действия.

-Автоматические переменные.

-Внешние переменные.

-Статические переменные.

-Внешние статические переменные.

-Регистровые переменные.

Классы памяти и область действия

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

/* глобальная переменная ext */

int ext; /* внешняя переменная */

main( ) { extern int ext;

printf("Сколько курсов на сайте intuit.ru?\n");

scanf("%d",&ext);

while(ext != 30)

critic( ); printf("Посмотрите на сайте!\n");

} critic( ) { extern int ext;

printf("Ошибка. Попытайтесь снова.\n");

scanf("%d",&ext);

} Результат:

Сколько курсов на сайте intuit.ru?

20 Ошибка. Попытайтесь снова.

30 Посмотрите на сайте!

Мы сделали переменную ext внешней, описав ее вне любого определения функции. Внутри функции, использующей эту переменную, мы объявляем ее внешней при помощи ключевого слова extern, предшествующего спецификации типа переменной. Компилятор ищет определение этой переменной вне функции. Если бы мы опустили ключевое слово extern в функции critic( ), то компилятор создал бы в функции critic новую переменную и тоже назвал бы ее ext. Тогда другая переменная ext, которая находится в main( ), никогда не получила бы нового значения.

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

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

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

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

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

Если класс памяти идентификатора не указан явно, то его класс памяти задается положением его определения в тексте программы. Если идентификатор определяется внутри функции, тогда его класс памяти auto, в остальных случаях идентификатор имеет класс памяти extern.

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

! При описании внешних массивов максимальное значение первого индекса массива указывать нет необходимости. Оно будет получено из соответствующего определения, что создает дополнительные удобства для пользователя. Для внешнего идентификатора память выделяется только в том случае, если класс памяти не указан явно.

! Явное указание памяти extern является отличительным признаком внешнего описания от внешнего определения. Хотя описание внешнего идентификатора может встретиться во многих файлах, только один файл должен содержать определение внешнего идентификатора. Область действия внешних идентификаторов не ограничивается файлом, содержащим их определения, а включает также файлы с соответствующими описаниями, с классом памяти extern.

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

Класс памяти позволяет установить два факта. Во-первых, определить, какие функции имеют доступ к переменной. Пределы, до которых переменная доступна, характеризуют ее область действия. Во-вторых, определить, как долго переменная находится в памяти. Теперь подробнее рассмотрим свойства каждого типа.

Автоматические переменные

По умолчанию переменные, описанные внутри функции, являются автоматическими. Можно, однако, это подчеркнуть явно с помощью ключевого слова auto:

main( ) { auto int kat;

} Так поступают, если хотят, например, показать, что определение переменной не нужно искать вне функции.

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

Автоматическая переменная начинает существовать при вызове функции, содержащей ее. Когда функция завершает свою работу и возвращает управление туда, откуда ее вызвали, автоматическая переменная исчезает. Область действия автоматической переменной ограничена блоком, т.е. { }, в котором переменная описана.

! Мы всегда должны описывать наши переменные в начале тела функции (блока). Областью действия их является вся функция. Можно описать переменную внутри подблока. Тогда переменная будет известна только в этой части функции, однако этого лучше не делать. Это дурной стиль программирования! Внешние переменные

Переменная, описанная вне функции, является внешней.

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

Пример: int global_flag;

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

Если слово extern не включено в описание внутри функции, то под этим именем создается новая автоматическая переменная. Мы можем пометить вторую переменную как автоматическую с помощью слова auto.

Статические переменные

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

Пример: /* Статическая переменная */

main( ) { int count

for(count = 1;count 0)?(A) : -(A))

Каждое вхождение выражения abs(arg) в тексте программы заменяется на

((arg) > 0) ? (arg) : -(arg),

причем параметр макроопределения А заменяется на arg.

Пример:

#define nmem (P,N)\

(P) -> p_mem[N].u_long

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

Макроопределение с аргументами очень похоже на функцию, поскольку аргументы его заключены в скобки:

/* макроопределение с аргументами */

#define SQUARE(x) x*x

#define PR(x) printf("x равно %d.\n", x)

main( ) { int x = 4; int z;

z = SQUARE(x);

PR(z); z = SQUARE(2);

PR(z); PR(SQUARE(x));

PR(SQUARE(x+2));

PR(100/SQUARE(2));

PR(SQUARE(++x));

} Всюду, где в нашей программе появляется макроопределение SQUARE(x), оно заменяется на x*x . В отличие от наших прежних примеров, при использовании этого макроопределения мы можем совершенно свободно применять символы, отличные от x. В макроопределении 'x' замещается символом, использованным в макровызове программы. Поэтому макроопределение SQUARE(2) замещается на 2*2. Таким образом, x действует как аргумент. Однако, аргумент макроопределения не работает - точно так же, как аргумент функции. Вот результаты выполнения программы:

z равно 16.

z равно 4.

SQUARE(x) равно 16.

SQUARE(x+2) равно 14.

100/SQUARE(2) равно 100.

SQUARE(++x) равно 30.

Первые две строки очевидны. Заметим, что даже внутри двойных кавычек в определении PR переменная замещается соответствующим аргументом. Все аргументы в этом определении замещаются. Рассмотрим третью строку:

PR(SQUARE(x)); Она становится следующей строкой:

printf("SQUARE(x) равно %d.\n", SQUARE(x));

после первого этапа макрорасширения. Второе SQUARE(x) расширяется, превращаясь в x*x, а первое остается без изменения, потому что теперь оно находится внутри кавычек в операторе программы, и таким образом защищено от дальнейшего расширения. Окончательно строка программы содержит

printf("SQUARE(x) равно %d.\n",x*x);

и выводит на печать

SQUARE(x) равно x*x.

Если макроопределение включает аргумент с двойными кавычками, то аргумент будет замещаться строкой из макровызова. Но после этого он в дальнейшем не расширяется, даже если строка является еще одним макроопределением. В нашем примере переменная x стала макроопределением SQUARE(x) и осталась им. Вспомним, что x=4. Это позволяет предположить, что SQUARE(x+2) будет равно 6*6 или 36. Но напечатанный результат говорит, что получается число 14. Причина такого результата такова: препроцессор не делает вычислений. Он только замещает строку. Всюду, где наше определение указывает на x, препроцессор подставит строку x+2.

Таким образом,

x*x становится x+2*x+2

Если x равно 4, то получается

4+2*4+2=4+8+2=14

Вызов функции передает значение аргумента в функцию во время выполнения программы. Макровызов передает строку аргументов в программу до ее компиляции.

Макроопределение или функция?

! Многие задачи можно решать, используя макроопределение с аргументами или функцию. Что из них следует применять? На этот счет нет строгих правил, но есть некоторые соображения. Макроопределения должны использоваться скорее как хитрости, а не как обычные функции. Они могут иметь нежелательные побочные эффекты. Некоторые компиляторы ограничивают макроопределения одной строкой, и, по-видимому, лучше соблюдать такое ограничение, даже если ваш компилятор этого не делает.

Выбор макроопределения приводит к увеличению объема памяти, а выбор функции - к увеличению времени работы программы. Макроопределение создает строчный код, т.е. мы получаем оператор в программе. Если макроопределение применить 20 раз, то в программу вставится 20 строк кода. Если мы используем функцию 20 раз, то у нас будет только одна копия операторов функции. Однако управление программой следует передать туда, где находится функция, а затем вернуться в вызывающую программу, а на это потребуется больше времени, чем при работе со строчными кодами. Так что думайте, что выбирать!

Преимущество макроопределений заключается в том, что при их использовании нам не нужно беспокоиться о типах переменных, т.к. макроопределения имеют дело с символьными строками, а не с фактическими значениями. Tак наше макроопределение SQUARE(x) можно использовать одинаково хорошо с переменными типа int или float.

Запомним!

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

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

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

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

Включение файла: #include

Перечень обозначений заголовочных файлов для работы с библиотеками компиляторов утвержден стандартом языка. Ниже приведены названия этих файлов, а также краткие сведения о тех описаниях и определениях, которые в них включены. Большинство описаний - прототипы стандартных функций, а определены в основном константы, например EOF, необходимые для работы с библиотечными функциями.

assert.h - диагностика программ

ctype.h - преобразование и проверка символов

errno.h - проверка ошибок

float.h - работа с вещественными данными

limits.h - предельные значения целочисленных данных

locale.h - поддержка национальной среды

math.h - математические вычисления

setjump.h - возможности нелокальных переходов

signal.h - обработка исключительных ситуаций

stdarg.h - поддержка переменного числа параметров

stddef.h - дополнительные определения

stdio.h - средства ввода-вывода

stdlib.h - функции общего назначения (работа с памятью)

string.h - работа со строками символов

time.h - определение дат и времени

В конкретных реализациях количество и наименование заголовочных файлов могут быть и другими. Например, в компиляторах для MS-DOS активно используются заголовочные файлы mem.h, alloc.h, conio.h, dos.h и другие. В компиляторах Turbo C, Borland C++ для связи с графической библиотекой применяется заголовочный файл graphics.h.

Командная строка #include может встречаться в любом месте программы, но обычно все включения размещаются в начале файла исходного текста.

#include Пример:

#include

Процессор заменяет эту строку содержимым файла math.h. Угловые скобки означают, что файл math.h будет взят из некоторого стандартного каталога (обычно это /usr/include). Текущий каталог не просматривается:

#include "имя_файла"

Пример: #include "ABC"

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

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

В конкретных реализациях количество и наименования заголовочных файлов могут быть разными:

#include ищет в системном каталоге

#include "my.h" ищет в текущем рабочем

каталоге

#include "/user/1/my.h" ищет в каталоге /user/1

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

#include "stdio.h" ищет на стандартном диске

#include ищет на стандартном диске

#include "a:stdio.h" ищет на диске а

По соглашению суффикс .h используется для заголовочных файлов, т.е. файлов с информацией, которая располагается в начале программы. Заголовочные файлы обычно состоят из операторов препроцессора.

Некоторые файлы включены в систему, например, stdio.h, но можно создать и свой файл.

Многие программисты разрабатывают свои стандартные заголовочные файлы, чтобы использовать их в программах.

Условная компиляция

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

#if константное_выражение

Пример: #if ABC + 3

Истина, если константное выражение ABC + 3 не равно нулю.

#ifdef идентификатор

Пример: #ifdef ABC

истина, если идентификатор ABC определен ранее командой #define.

#ifndef идентификатор

Пример: #ifndef ABC

истина, если идентификатор ABC не определен в настоящий момент.

#else . . . #endif

Если предшествующие проверки #if, #ifdef или #ifndef дают значение "Истина", то строки от #else до #endif игнорируются при компиляции.

Если эти проверки дают значение "Ложь", то строчки от проверки до #else (а при отсутствии #else - до #endif) игнорируются.

Команда #endif обозначает конец условной компиляции.

Пример: #ifdef DEBUG

fprintf (stderr, "location: x = %d\n", x);

#endif Вспомогательные директивы

Номер строки и имя файла

#line целая_константа "имя_файла"

Пример: #line 20 "ABC"

Препроцессор изменяет номер текущей строки и имя компилируемого файла. Имя файла может быть опущено.

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

Пример:

#define N 3/*определение константы */

void main( ) { #line 55 "file.c"

double x[3*N]; }

Реакция на ошибки

#error последовательность лексем

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

Пример: #define NAME 15

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

#if (NAME !=15)

#error NAME должно быть равно 15!

Сообщение будет выглядеть так:

error ; error directive: NAME должно быть равно 15!

Пустая директива

# Использование этой директивы не вызывает никаких действий.

Прагмы #pragma

Эта директива определяет действия, зависящие от конкретной реализации компилятора. Например в некоторые компиляторы входит вариант этой директивы для извещения компилятора о наличии в тексте программы команд на языке Ассемблер. Возможности команды #pragma могут быть разнообазными. Стандарта для них не существует. Если конкретный препроцессор встречает прагму, которая ему неизвестна, он ее просто игнорирует как пустую директиву. В некоторых реализациях включена прагма.

#pragma pack(n), где n= 1,2,4. Прагма pack позволяет влиять на упаковку смежных элементов в структурах и объединениях (см. лекцию 14).

Соглашение может быть таким:

pack(1) - выравнивание элементов по границам байтов;

pack(2) - выравнивание элементов по границам слов;

pack(4) - выравнивание элементов по границам двойных слов;

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

Встроенные макроимена

Существуют встроенные (заранее определенные) макроимена, доступные препроцессору во время обработки. Они позволяют получить следующую информацию:

_DATA_ - строка символов в формате: "месяц число год", определяющая дату начала обработки исходного файла. Например, после препроцессорной обработки текста программы, выполненной 29 января 2005 года, оператор

printf(_DATA_); станет таким

printf("January 29 2005");

_LINE_ - десятичная константа - номер текущей обрабатываемой строки файла с программой на Си. Принято, что номер первой строки исходного файла равен 1;

_FILE_ - строка символов - имя компилируемого файла. Имя изменяется всякий раз, когда препроцессор встречает директиву #include с указанием имени другого файла. Когда включения файла по команде #include завершаются, востанавливается предыдущее значение макроимени _FILE_;

_TIME_ - строка символов вида "часы:минуты:секунды", определяющая время начала обработки препроцессором исходного файла;

_STDC_ - константа, равная 1, если компилятор работает в соответствии с ANSI-стандартом. В противном случае значение микроимени _STDC_ не определено. Стандарт языка Си предполагает, что наличие имени _STDC_ определяется реализацией, так как макрос _STDC_ относится к нововведениям стандарта. В конкретных реализациях набор предопределенных имен гораздо шире. Для получения более полных сведений о предопределенных препроцессорных именах следует обращаться к документации по конкретному компилятору.

12. Лекция: Массивы и указатели.

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

-Массивы.

-Указатели.

-Динамические объекты.

-Создание динамических объектов.

-Доступ к динамическим объектам.

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

-Инициализация массивов и классы памяти.

-Функции.

-Операции с указателями.

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

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

Указатель на основной тип:

char *p; Переменная р является указателем на символ, т.е. этой переменной должен присваиваться адрес символа.

Указатель на указатель:

char **t; Переменная t - указатель на указатель символа.

Одномерный массив:

int a[50]; Переменная а - массив из 50 целых чисел.

Двумерный массив:

char m[7][50]; Переменная m - массив из семи массивов, каждый из которых состоит из 50 символов.

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

char *r[7]; Массив r состоит из указателей на символы.

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

int (*f)(); f - указатель на функцию, возвращающую целое значение.

Массивы

Массив является сложным объектом, состоящим из объектов-компонентов, называемых элементами одного и того же типа. Простые определения массива имеют вид

Тип данных x[n1][n2]...[nk]

Где x - идентификатор, определяемый в качестве имени массива, а ni - размерности массива. Массив x называется k-мерным массивом с элементами типа тип данных. Элементы i-го измерения имеют индексы от 0 до ni-1. Тип элемента массива может быть одним из основных типов, типом другого массива, типом указателя (pointer), типом структуры (struct) или типом объединения (union). Хотя элементы массива не могут быть функциями, они могут быть указателями на функции. Ниже приведены некоторые примеры определений массива:

int page[10]; /* одномерный массив из 10

элементов, перенумерованный с 0 до 9 */

char line[81]; float big[10][10], sales[10][5][8]; /*двумерный

массив и трехмерный массив*/

Ссылки на элемент k-мерного массива x делаются с помощью следующего обозначения:

x[i1][i2]...[ik]

где ij - целое выражение, при этом 01) массив, является ссылкой на (n-1)-мерный подмассив с элементами y[i, j2,j3,_jn], где значения jk соответствуют определению массива y. y[i] дает адрес первого элемента этого подмассива, т.е. *(y+i). Все элементы этого (n-1)-мерного подмассива занимают непрерывную область памяти.

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

Строки - это массивы знаков. По соглашению, последним знаком строки должен быть нулевой знак \0. Поскольку имя массива фактически является указателем на первый элемент массива, переменные типа string могут также рассматриваться, как имеющие тип char *. Например, вторая переменная string_array в определении

char *string_pointer, string_array[81];

может рассматриваться также как знаковый указатель. Для строки, представленной первой переменной string_pointer, память должна быть выделена явно. С другой стороны, для массива string_array память является указателем на нее. Заметим, что память должна быть также выделена или зарезервирована для признака конца строки \0.

! Нет ничего необычного не только в интерпретации переменных типа string, т.е. массивов знаков как указателей, но и в интерпретации строк, которые также могут рассматриваться двояко - как массивы и как указатели - и все в одной программе! Это особенно важно, когда строки передаются как аргументы функции. Вызывающая программа может рассматривать строку как массив знаков, а вызываемая функция может рассматривать ее как знаковый указатель. Если длина строки непостоянна, то использование знаковых указателей для строк имеет определенные преимущества. Хотя строки переменной длины могут быть также реализованы с использованием массивов, такая реализация оказывается слишком неэкономной с точки зрения использования памяти и налагает ограничения на максимальную длину строки. Например, для размещения строк разной длины может быть создан массив знаковых указателей. Альтернативное решение с использованием двумерного массива знаков в общем случае будет использовать память неэффективно, так как в этом случае потребовалось бы сделать число столбцов равным числу знаков в строке наибольшей возможной длины. Инициализация массивов и классы памяти

Мы знаем, что скалярные переменные можно инициализировать в описании типа при помощи таких выражений, как например:

int fix = 1;

float flax = PI*2;

при этом предполагается, что PI - ранее введенное макроопределение. Можно ли инициализировать массивы?

Внешние, статические и автоматические массивы можно инициализировать!

Регистровые массивы инициализировать нельзя!

Если ничего не засылать в массив перед началом работы с ним, то внешние, статические и автоматические массивы инициализируются для числовых типов нулем и '\0' (null) для символьных типов, а регистровые массивы содержат какой-то мусор, оставшийся в этой части памяти. Если в статическом, внешнем или автоматическом массиве нам нужны первоначальные значения, отличные от нуля, в этом случае мы можем делать так:

/* дни месяца */

int days[12]={31,28,31,30,31,30,31,31,30,31,30,31};

main( )

{ int index;

extern int days[];/*необязательное описание */

for(index = 0; index

strcat - сцепить две строки.

Определение: char *strcat(s1,s2)

char *s1, *s2; Пример 1:

/* сцепить две строки */

/* в головном файле conio.h содержится функция очистки экрана clrscr( ) */

#include

#include #include

int main(void) { clrscr();

char destination[25];

char *blank = " ", *c = "C++", *turbo = "Turbo";

strcpy(destination, turbo);

strcat(destination, blank);

strcat(destination, c);

printf("%s\n", destination);

getch(); return 0;

} strncat - сцепить две строки, причем из второй строки копировать не более n символов.

Определение: char *strncat(s1,s2,n)

char *s1, *s2;

int n; Пример 2:

/* cцепить две строки, причем из второй строки

копировать не более n символов */

#include

#include #include

int main(void) {

clrscr(); char destination[25];

char *source = "structured ";

strcpy(destination, "programming");

strncat(destination, source, 11);

printf("%s\n", destination);

getch();

return 0; } strcmp - сравнить две строки в лексикографическом порядке.

Определение: int strcmp(s1,s2)

char *s1, *s2; Пример 3:

#include

#include #include

int main(void) {

char *buf1 = "aaa", *buf2 = "bbb", *buf3 = "ccc";

int ptr;

clrscr(); ptr = strcmp(buf2, buf1);

if (ptr > 0) printf("buffer 2 is greater than buffer 1\n");

else printf("buffer 2 is less than buffer 1\n");

ptr = strcmp(buf2, buf3);

if (ptr > 0) printf("buffer 2 is greater than buffer 3\n");

else printf("buffer 2 is less than buffer 3\n");

getch(); return 0;

} strncmp - сравнить первые n символов двух строк.

Определение: int strncmp(s1,s2, n)

char *s1, *s2; int n;

Пример 4: #include

#include

#include int main(void)

{ char *buf1 = "aaabbb", *buf2 = "bbbccc", *buf3 = "ccc";

int ptr; clrscr();

ptr = strncmp(buf2,buf1,3);

if (ptr > 0) printf("buffer 2 is greater than buffer 1\n");

else printf("buffer 2 is less than buffer 1\n");

ptr = strncmp(buf2,buf3,3);

if (ptr > 0)

printf("buffer 2 is greater than buffer 3\n");

else printf("buffer 2 is less than buffer 3\n");

getch(); return(0);

} strcpy - копировать строку s2 в строку s1.

Определение: char *strcpy(s1,s2)

char *s1, *s2; Пример 5:

#include #include

#include

int main(void) {

clrscr(); char string[10];

char *str1 = "abcdefghi";

strcpy(string, str1);

printf("%s\n", string);

getch(); return 0;

} strncpy - копировать не более n символов строки s2.

Определение: char *strncpy(s1,s2,n)

char *s1, *s2; int n;

Пример 6: #include

#include #include

int main(void)

{ clrscr(); char string[10];

char *str1 = "abcdefghi";

strncpy(string, str1, 3);

string[3] = '\0';

printf("%s\n", string);

getch(); return 0;

} strlen - определить длину строки (число символов без завершающего нулевого символа).

Определение: int strlen(s)

char *s; Пример 7:

#include #include

#include int main(void)

{ clrscr();

char *string = "Borland International";

printf("%d\n", strlen(string));

getch(); return 0;

} strchr - найти в строке первое вхождение символа с.

Определение:

char *strchr(s,n)

char *s; int n;

Пример 8:

#include #include

#include int main(void)

{ clrscr(); char string[20];

char *ptr, c = 'r';

strcpy(string, "This is a string");

ptr = strchr(string, c);

if (ptr) printf("The character %c is at position: %d\n", c, ptr);

else

printf("The character was not found\n");

getch(); return 0;

} strrchr - найти в строке последнее вхождение символа с.

Определение:

char *strrchr(s,c)

char *s; int c;

Пример 9: #include

#include

#include int main(void)

{ clrscr(); char string[20];

char *ptr, c = 'r';

strcpy(string, "This is a string");

ptr = strrchr(string, c);

if (ptr) printf("The character %c is at position: %d\n", c, ptr);

else printf("The character was not found\n");

getch();

return 0; } strpbrk - найти в строке s1 любой из множества символов, входящих в строку s2.

Определение:

char *strpbrk(s1,s2)

char *s1, *s2;

Пример 10:

#include #include

#include int main(void)

{ clrscr();

char *string1 = "abcdefghijklmnopqrstuvwxyz";

char *string2 = "onm";

int *ptr; ptr = strpbrk(string1, string2);

if (ptr)

printf("strpbrk found first character: %c\n", ptr);

else printf("strpbrk didn't find character in set\n");

getch(); return 0;

} strspn - определить длину отрезка строки s1, содержащего символы из множества, входящих в строку s2.

Определение:

int strspn(s1,s2)

char *s1, *s2; Пример 11:

#include #include

#include

#include int main(void)

{ clrscr(); char *string1 = "1234567890";

char *string2 = "123DC8";

int length;

length = strspn(string1, string2);

printf("Character where strings differ is at position %d\n", length);

getch(); return 0;

} strcspn - определить длину отрезка строки s1, не содержащего символы cтроки s2.

Определение:

int strcspn(s1,s2)

char *s1, *s2; Пример 12:

#include #include

#include

#include int main(void)

{ clrscr(); char *string1 = "1234567890";

char *string2 = "747DC8";

int length;

length = strcspn(string1, string2);

printf("Character where strings intersect is at position %d\n", length);

getch(); return 0;

} strtok - выделить из строки s1 лексемы, разделенные любым из множества символов, входящих в строку s2.

Определение: char *strtok(s1,s2)

char *s1, *s2; Пример 13:

#include #include

#include int main(void)

{ clrscr();

char input[16] = "abc,d";

char *p; p = strtok(input, ",");

if (p) printf("%s\n", p);

p = strtok(NULL, ",");

if (p) printf("%s\n", p);

getch(); return 0;

}

14. Лекция: Структуры.

-Определение структурных переменных.

-Доступ к компонентам структуры.

-Поля битов в структурах.

-Объединения.

-Перечисления.

-Переменные структуры.

-Указатели структуры.

-Массив структур.

-Переименования типов.

Определение структурных переменных

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

struct имя_структуры {

Описание_элементов

}; Пример:

struct dinner { char *plase;

float cost; struct dinner *next;

}; Структурная переменная описывается с помощью переменной структурного типа.

Примеры:

struct dinner week_days [7]; /* массив структур */

struct dinner best_one; /* одна структурная переменная */

struct dinner *p; /* указатель на структурную переменную */

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

struct{

список описаний

} В структуре должен быть указан хотя бы один компонент. Указатель типа структуры используется для определения структур. Определения структур имеют следующий вид:

тип-данных описатели;

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

struct {

double x,y; } a,b,c[9];

переменные a и b определяются как структуры, каждая из которых состоит из двух компонентов - x и y. Переменная с определяется как массив из девяти таких структур.

Из определения

struct {

int year; short int month, day;

} date1,date2; следует, что каждая из двух переменных date1, date2 состоит из трех компонентов: year, month, day.

С типом структуры может быть ассоциировано имя, которое задается описанием типа в форме

typedef struct {

список описаний

} имя-типа-структуры;

Спецификатор typedef (определяет класс памяти) позволяет нам создать свое собственное имя типа. Это напоминает директиву #define, но со следующими тремя изменениями:

В отличие от #define спецификатор typedef дает символические имена, но ограничивается только типами данных.

Спецификатор typedef выполняется компилятором, а не препроцессором.

В своих пределах спецификатор typedef более гибок, чем #define.

В дальнейшем эти имена могут использоваться для определения структур. Ниже приведен пример описания типа структуры с именем employee:

typedef struct {

char name[30];

int id; dept d;

family f; } employee;

где слова dept, family указывают типы, а именно типы структур, предварительно определенные пользователем. Тип структуры employee может быть использован для определения переменных. Например, определение

employee chairperson, president, e1, e2;

описывает переменные chairperson, president, e1, e2 как структуры типа employee.

Существует и другой способ ассоциирования имени с типом структуры. Этот способ основан на применении меток структуры. Метки структуры аналогичны меткам перечисляемого типа. Метка структуры описывается следующим образом:

struct метка{

список описаний

} где метка является идентификатором. В приведенном ниже примере слово student описывается как метка структуры:

struct student {

char name[25];

int id,age;

char sex; };

Метки структуры используются для определения структур записью вида

struct метка список-идентификаторов;

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

struct node {

int data; struct node *next;

}; метка структуры node действительно является рекурсивной, так как она используется в своем собственном описании, т.е. в описании указателя next. Из-за наличия знака * переменная next описана как указатель на объекты типа node. Структуры не могут быть прямо рекурсивными. Структура типа S не может содержать компонент, являющийся структурой типа S. Однако структура типа S может содержать компонент, указывающий на структуру типа S.

Доступ к компонентам структуры

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

s.c где s является именем структуры или значением структуры с компонентом c. s может быть выражением, дающим в результате значение структуры. Например, s может быть вызовом функции со структурой в качестве ее значения. К компонентам определенной выше структуры date1 можно обратиться, указав их обозначения:

date1.year

date1.month date1.day

Поля битов в структурах

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

Пример:

struct bfeg { unsigned int bf_flg1 : 10;

unsigned int bf_flg2 : 6;

}; Данная структура описывает 10-битовое поле, которое для вычислений преобразуется в значение типа unsigned int, и 6-битовое поле, которое обрабатывается как значение типа unsigned int.

Объединения

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

Определение объединенного типа данных аналогично определению структурного типа данных:

union имя_объединения {

Описания_элементов

}; Пример:

union bigword { long bg_long;

char *bg_char [4];

}; Данные типа union bigword занимают память, необходимую для размещения наибольшего из своих элементов, и выравниваются в памяти к границе, удовлетворяющей ограничениям по адресации как для типа long, так и для типа char *[4].

Описание переменной объединенного типа:

Пример: union bigword x;

union bigword *p;

union bigword a[100];

Перечисления

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

Определение перечислимого типа данных:

enum имя_перечислимого_типа {

Список_значений

}; Каждое значение данного перечислимого типа задается идентификатором.

Пример: enum color {

red, green, yellow

}; Описание переменной перечислимого типа:

enum color chair;

enum color suite [40];

Использование переменной перечислимого типа в выражении.

Пример:

chair = red; suite[5] != yellow;

Переменные структуры

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

struct {

общие компоненты;

метка активного компонента;

union { описание компонента 1

описание компонента 2

... описание компонента n

} идентификатор;

} Ниже приведен пример определения переменной структуры health_record:

struct {

/* общая информация */

char name[25];

int age; char sex;

/* метка активного компонента */

marital_status ms;

/* переменная часть */

union { /* холост */

/* нет компонентов */

/* женат */

struct { char marriage_date[8];

char spouse_name[25];

int no_children;

} /* разведен */

char date_divorced[8];

} marital_info;

} health_record;

где тип marital_status, т.е. тип метки активного компонента ms, описан как

typedef enum {SINGL,MARRIED, DIVORCED}

marital_status;

Ниже приведены несколько примеров ссылки на компоненты переменной структуры:

health_record.name

healts_record.ms

health_record.marital_info.marriage_date

Указатели и структуры

Рассмотрим метку структуры student, описание которой было дано выше как

struct student {

char name[25];

int id, age; char sex;

} Указатель new_student определен как

struct student *new_student;

Предположим, что память выделена таким образом, чтобы new_student указывал на объект student. Тогда на компоненты этого объекта можно ссылаться следующим образом:

(*new_student).name

(*new_student).id

(*new_student).age

(*new_student).sex

Поскольку указатели часто используются для указания на структуры, в языке Си специально для ссылок на компоненты таких структур введен оператор выбора стрелка вправо ->. Например, ссылки на вышеприведенные компоненты структуры можно записать с использованием оператора стрелки вправо -> как:

new_student->name

new_student->id

new_student->age

new_student->sex

Массив структур

Процесс описания массива структур совершенно аналогичен описанию любого другого типа массива:

struct book libry[MAXBKS];

Этот оператор объявляет libry массивом, состоящим из MAXBKS-элементов. Каждый элемент массива представляет собой структуру типа book. Таким образом, libry[0] является book-структурой, libry[1] - второй book-структурой и т.д.

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

libry[0].value value - первый элемент массива

libry[4].title title - пятый элемент массива

Переименование типов

Формат typedef старый_тип новый_тип

Примеры: typedef long large;

/* определяется тип large, эквивалентный типу long */

typedef char *string;

/* тип string, эквивалентен типу char* */

Переименование типов используется для введения осмысленных или сокращенных имен типов, что повышает понятность программ, и для улучшения переносимости программ (имена одного типа данных могут различаться на разных ЭВМ).

Пример:

/* Реализован алгоритм, который позволяет определить

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

расположенных в различных столбцах. Используются

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

сортировка строк по возрастанию. Отсортированные

строки сравниваются и выводятся на экран номера

одинаковых строк */

#include

#include #include

#include #define n 4 /*количество строк */

#define m 4 /*количество столбцов*/

typedef struct mas{int i,i1;} mas;

int m1[n][m]; /*исходный массив*/

struct mas{int i,i1;};

mas a[n*2]; /*массив типа mas, где будут лежать одинаковые

строки, a[1].i и a[1].i1 - одинаковые строки*/

void main() { clrscr();

int i,j; randomize();

for(i=0;i=min) {

m1[i][p]=m1[i][s];m1[i][s]=min;

} /* меняем местами s-й и p-й элемент,если

s-й > p-го(минимального) */

} } printf("\n");

for(i=0;i

Кроме указанных, в большинстве UNIX-систем есть файлы заголовков, которые не определены в ANSI C:

. В этих файлах заголовков объявляются функции, которые помогают получить доступ к информации о бюджетах пользователей и групп в UNIX-системах. В указанных системах они определены в библиотеке libc.a. Эти функции полезны для разработки приложений.

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

Потоковая функция или макрокоманда Назначение fopen Открывает поток для чтения и (или) записи fclose Закрывает поток fread Читает блок данных из потока fgets Читает строку текста из потока fscanf Читает форматированные данные из потока fwrite Записывает блок данных в поток fputs Записывает строку текста в поток fprintf Записывает форматированные данные в поток fseek Перемещает указатель чтения или записи в потоке ftell Возвращает текущую позицию в потоке, начиная с которой будет выполнена следующая операция чтения или записи. Возвращаемое значение - это количество байтов смещения относительно начала потока freopen Повторно использует указатель потока для ссылки на новый файл fdopen Открывает потоковый файл с указанным дескриптором feof Макрокоманда, которая возвращает ненулевое значение, если в данном потоке обнаружен символ конца файла, в противном случае - нулевое значение ferror Макрокоманда, которая возвращает ненулевое значение, если в данном потоке была обнаружена ошибка или символ конца файла, в противном случае - нулевое значение clearer Макрокоманда, которая сбрасывает флаг наличия ошибок в данном потоке fileno Макрокоманда, которая возвращает дескриптор данного потокового файла В заголовке объявляется набор функций, служащих для преобразования данных, генерации случайных чисел, получения и установки переменных среды shell, управления выполнением программ и выполнения команд shell. Обычно эти функции объявляются в заголовке , но так как они не включают в себя манипулирование потоками, стандарт ANSI C группирует их в отдельный заголовок.

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

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

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

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

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

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

В заголовке определяется набор функций, предназначенных для получения учетной информации о группах, содержащейся в UNIX-файле /etc/group.

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

Доступ в библиотеку языка Си

Получение доступа к библиотеке зависит от системы. Во-первых, есть несколько различных мест расположения библиотечных функций, Например, getchar( ) обычно задают как макроопределение в файле stdio.h, в то время как strlen( ) обычно хранится в библиотечном файле. Во-вторых, различные системы имеют разные способы доступа к этим функциям. Вот три из них.

Автоматический доступ

Во многих больших системах UNIX вы только компилируете программы. А доступ к более общим библиотечным функциям выполняется автоматически.

Включение файла

Если функция задана как макроопределение, то можно директивой #include включить файл, содержащий ее определение. Часто подобные функции могут быть собраны в соответствующим образом названный заголовочный файл. Например, некоторые системы имеют файл ctype.h, содержащий макроопределения, задающие тип символа (прописная буква, цифра и т.д.)

Включение библиотеки

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

Очевидно, мы не сможем рассмотреть все особенности всех систем, но эти три примера показывают, что вас ожидает!

Связь с файлами

Один способ организации связи программы с файлом заключается в использовании операций переключения . Этот метод прост, но ограничен. Язык Си предоставляет и более мощные методы связи с файлами. Рассмотрим использование функции fopen( ), которая открывает файл, затем применяются специальные функции ввода-вывода для чтения файла или записи в этот файл и далее используется функция fclose( ) для закрытия файла. Прежде чем исследовать эти функции, кратко познакомимся с сущностью файла.

Файл является частью памяти, обычно на диске, со своим именем. Мы считаем, что он содержит некоторую полезную информацию. Для операционной системы файл более сложен, но это системные проблемы, а не наши. Но мы должны знать, что означает файл для программы на языке Си. В предлагаемых для обсуждения функциях, работающих с файлами, язык Си рассматривает файл как структуру. Вот типичный пример, взятый из IBM-версии компилятора Lattice C:

struct_iobuf { char*_ptr; /* текущий указатель буфера*/

int_cnt; /* текущий счетчик байтов*/

char*_base; /* базовый адрес буфера ввода-вывода*/

char_flag; /* управляющий признак*/

char_file; /* номер файла*/

} #define FILE struct_iobuf /* краткая запись*/

Здесь мы не собираемся разбираться детально в этом определении. Главное состоит в том, что файл является структурой, и что краткое наименование шаблона - FILE. Многие системы используют директиву typedef для установления этого соответствия. Таким образом, программа, имеющая дело с файлами, будет использовать тип структуры FILE, чтобы делать так.

Рассмотрим пример чтения содержимого файла, названного File, и вывода его на экран:

#include main( )

{ FILE *in; /* описываем указатель на файл */

int ch; if ((in = fopen("File", "r") ) != NULL) {

/* открываем File для чтения,

проверяя существует ли он */

/* указатель FILE ссылается теперь на File */

while ((ch = getc(in) != EOF))

/* получаем символ из in */

putc(ch, stdout);

/* посылаем ch на стандартный вывод*/

fclose(in); /* закрываем фаил */

} else printf (" Файл не открывается\"File\".\n);

} Объясним работу: fopen( ), fclose и использование функций ввода-вывода файла.

Открытие файла: fopen( )

Функцией fopen( ) управляют три основных параметра. Первый - имя файла, который следует открыть. Он является и первым аргументом fopen( ). В нашем примере это "File". Второй параметр описывает, как должен использоваться файл:

"r" - файл нужно считать,

"w" - файл нужно записать,

"a" - файл нужно дополнить.

"w+" - новый текстовый файл открывается для записи и последующих многократных исправлений. Если файл уже существует, то предыдущее содержимое стирается. Последующие после открытия файла запись и чтение из него допустимы в любом месте файла, в том числе запись разрешена и в конце файла, т.е. файл может увеличиваться.

"r+" - существующий текстовый файл открывается как для чтения, так и для записи в любом месте файла; однако в этом режиме невозможна запись в конец файла, то есть недопустимо увеличение размеров файла.

"a+" - текстовый файл открывается или создается, если файла нет, и становится доступным для изменений, т.е. для записи и для чтения в любом месте; при этом в отличие от режима "w+"можно открыть существующий файл и не уничтожать его содержимое; в отличие от режима "r+" в режиме "a+" можно вести запись в конец файла, то есть увеличивать его размеры.

Некоторые системы предоставляют еще дополнительные возможности, которые мы здесь не будем рассматривать. Используемые коды являются строками, а не символьными константами. При применении "r" открывается существующий файл. При двух других применениях тоже будет открываться существующий файл, но если такого файла нет, он будет создан. Если вы используете "w" для существующего файла, то старая версия его стирается, и ваша программа начинает записывать на чистое место. Третий параметр является указателем на файл. Это значение возвращается самой функцией:

FILE *in;

in=fopen("File","r");

Теперь in является указателем на файл "File". С этого момента программа ссылается на файл при помощи указателя in, а не по имени File. (Файл stdio.h содержит строку

FILE *fopen( ) Если fopen( ) не способна открыть требуемый файл, она возвращает значение NULL, определенное в stdio.h как 0.

Закрытие файла: fclose( )

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

fclose(in); Аргумент функции является указателем на файл. Для более серьезной программы нужно смотреть, успешно ли закрыт файл. Функция fclose( ) возвращает значение 0, если файл закрыт успешно, и EOF в противном случае.

Текстовые файлы с буферизацией

Функции fopen( ) и fclose( ) работают с текстовыми файлами с "буферизацией". Под буферизацией мы понимаем, что вводимые и выводимые данные запоминаются во временной области памяти, называемой буфером. Если буфер заполнился, содержимое его передается в блок, и процесс буферизации начинается снова. Одна из основных задач fclose( ) заключается в том, чтобы освободить любые частично заполненные буферы, если файл закрыт. Текстовым считается файл, в котором информация запоминается в виде символов в коде ASCII или аналогичном. Текстовый файл отличается от двоичного файла, который обычно используется для запоминания кодов машинного языка.

Ввод-вывод текстового файла: getc( ), putc( )

Две функции getc( ) и putc( ) работают аналогично функциям getchar( ) и putchar( ) (описанным в предыдущих лекциях). Разница заключается в том, что вы должны сообщить, какой файл следует использовать.

char ch; ch=getchar( );

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

ch=getc(in);

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

putchar(ch); выводит символ на стандартный файл вывода.

putc(ch,t); предназначена для записи символа ch в файл, на который ссылается указатель t типа FILE.

Ввод-вывод файла: fprintf( ), fscanf( ), fgets( ), fputs( )

Все функции ввода-вывода, которые мы использовали в предыдущих лекциях, имеют аналоги для ввода-вывода файла. Основное отличие состоит в том, что нам нужно использовать указатель типа FILE , чтобы сообщить функциям с каким файлом им следует работать. Подобно getc( ) и putc( ) эти функции используются после функции fopen( ), открывающей файл, и перед fclose( ), закрывающей его.

Функции fprintf( ) и fscanf( )

Эти функции ввода-вывода работают почти как printf( ) и scanf( ) (см. лекцию 4), но им нужен дополнительный аргумент для ссылки на сам файл. Он является первым в списке аргументов. Пример, иллюстрирующий обращение к этим функциям:

#include

main( ) { FILE *fi;

int age; fi=fopen("File","r"); /* считывание */

fscanf(fi,"%d",&age); /* fi указывает на File */

fclose(fi);

fi=fopen("Data", "a"); /*дополнение*/

fprintf(fi,"Data is %d.\n",age);

/*fi указывает на Data*/

fclose(fi);

} В отличие от getc( ) и putc( ) эти функции получают указатель типа FILE в качестве первого аргумента.

Функция fgets( )

Эта функция имеет три аргумента, в то время как gets( ) имеет лишь один. Пример ее использования:

/* Программа считывает файл строка за строкой */

#include #define MAX 80

main( ) { FILE *f1;

char *string[MAX]

f1=fopen("File","r");

while (fgets(string,MAX,f1) != NULL)

puts(string);

} Мы расположили вводимую информацию в символьном массиве string. Первый из трех аргументов функции fgets( ) является указателем на местоположение считываемой строки. Второй аргумент содержит предельную длину считываемой строки. Функция прекращает работу после считывания символа новой строки или после считывания символов общим числом MAX-1, в зависимости от того, что произойдет раньше. В любом случае нуль-символ '\0' добавляется в самый конец строки. Третий аргумент указывает на файл, который будет читаться. Разница между gets( ) и fgets( ) заключается в том, что gets( ) заменяет символ новой строки на '\0', в то время как fgets( ) сохраняет символ новой строки. Подобно gets( ) функция fgets( ) возвращает значение NULL, если встречает символ EOF . Это позволяет нам проверить, достигли ли мы конца файла.

Функция fputs( )

Эта функция похожа на функцию puts( ). Оператор

l=fputs("Строка", fi);

Код Положение в файле 0 начало файла 1 текущая позиция 2 конец файла Передает строку "Строка" в файл, на который ссылается указатель fi типа FILE. Конечно, сначала нужно открыть файл при помощи функции fopen( ).

l является целым числом, которое устанавливается в EOF, если fputs( ) встречает EOF или ошибку. Эта функция не ставит завершающий символ '\0' в конце копируемой строки. В отличии puts функция fputs( ) не добавляет символ новой строки в ее вывод.

Функция fseek( )

Функция fseek( ) позволяет нам обрабатывать файл подобно массиву и непосредственно достигать любого определенного байта в файле, открытом функцией fopen( ). fseek( ) имеет три аргумента и возвращает значение типа int.

Покажем на примере работу fseek( ):

/* использование fseek( ) для печати содержимого файла */

#include

int main(int number, char *names[])

{ FILE *fp; long set = 0L;

if(number #include

#include

#define STOP "\n" /* сигнал прекращения ввода */

#define BLOCK 100 /*байты памяти */

#define LIM 40 /*предельная длина вводимой строки*/

#define MAX 50 /*максимальное число вводимых строк */

#define TIME 20000 /* большая задержка времени */

main ( ) { char store[BLOCK]; /* исходный блок памяти*/

char symph[LIM]; /* приемник вводимых строк*/

char *end; /* указывает на конец памяти */

char *starts[MAX]; /* указывает на начала строк*/

int index = 0; /*количество вводимых строк */

int count; /* счетчик*/

сhar malloc( ); / * распределитель памяти */

starts[0]=store;

end=starts[0]+BLOCK-1;

puts("Вводите строки по одной");

puts("для завершения ввода в начале строке нажимите клавишу [ввод]");

puts("Начинайте!.");

while(indexend - starts[index]) {

/* действия при недостатке памяти для

запоминания вводимых данных*/

puts("подождите, программа попробует найти

дополнительную память");

starts[index]=malloc(BLOCK);

end=starts[index]+BLOCK- 1;

for(count=0; count

#include void main(void)

{ clrscr(); int mas[15][15];

int n=1,x=6,y=6,k=1;

int i,j;

while(1){ mas[x][y]=k++;

switch(n){

case 1: x++;break;

case 2: y--;break;

case 3: x--;break;

case 4: y++;break;

} if(x==15) break;

if(x==y && x6) n=3;

else if(x==y+1 && x>6) n=2;

} for(i=0;i2)

for(k=2;k

#include

#include #include

#include struct elem

{ int num; /* Номер вершины */

int suns; /* Количество сыновей */

char str[20]; /* Строка с номерами сыновей */

elem *next; /* Указатель на следующую вершину */

} *head, *w1, *w2;

int connected(int i, int j)

{ int k; char *str1;

w2 = head; if(i == j) return 0;

for(k=1; knext;

if( strchr(w2->str, j) ) return 1;

return 0; } void main()

{ int tops; int i,j,k,l;

char *str1; clrscr();

printf("Введите количество вершин \n");

scanf("%d", &tops);

head = (elem *)malloc(sizeof(elem));

head->num = 1;

head->suns = 0;

head->str[0] = '\0';

head->next = NULL;

w1 = head; for(i=2;inum = i;

w2->suns = 0;

w2->str[0] = '\0';

w2->next = NULL;

w1->next = w2;

w1 = w2;

} w1 = head;

for(i=1; i tops)) {

printf("Такой вершины нет,

повторите попытку\n");

l = 0;

j--;

continue;

}

w1->str[w1->suns++] = l;

w1->str[w1->suns] = '\0';

if(w1->suns == 49) {

printf("Слишком много связей !");

exit(1);

} }

w1 = w1->next;

}

clrscr(); printf("\n\n Матрица инциндентности :\n");

for(i=1; i1){sp=sp-1;x=stack[sp]} //антипереполнение

Чтение данных из указателя стека без извлечения элемента:

x=stack[sp-1];

Очереди Очередь - одномерная структура данных, для которой загрузка или извлечение элементов осуществляется с помощью указателей начала (head) и конца (tail) очереди в соответствии с правилом FIFO (first-in, first-out - первым введен, первым выведен).

Начальная установка:

head=1; tail=1; Добавление элемента:

queue[tail]=x; tail=tail+1;

if(tail>qd) tail=1;

Здесь qd - размерность очереди.

Исключение элемента:

x=queue[head]; head=head+1;

if(head>qd) tail=1;

Проверка переполнения очереди и включение в нее элемента:

temp=tail+1; if(temp=head)

{Переполнение}

else {queue[tail]=x; tail=temp}

Проверка наличия элементов и исключение элемента х:

if(head==tail) { очередь пуста}

else{ x=queue[head]; head=head+1;

if(head>qd) head=1;}

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

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

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

Приведем основные базисные операции для работы с однонаправленным связанным списком.

Включение элемента после элемента p:

link[q]=link[p];

link[p]=q; Здесь q - индекс элемента, который должен быть вставлен в список после элемента с индексом.

Исключение преемника элемента x:

if(link[x] != NULL) link[x]=[link[x]]

else {элемент не имеет преемника}

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

Включение элемента y перед элементом x:

prev=0;

while((link[prev]!=null) && (link[prev]!=x)) do

prev=link[prev];

if (link[prev]==x) {

link[prev]=y; link[y]=x

} else { элемент x не найден };

Здесь link[0] является началом списка.

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

В двунаправленном связанном списке каждый элемент имеет два указателя (succlink - описывает связь элемента с преемником, predlink - с предшественником).

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

Включение элемента перед элементом x:

succlink[y]=x; predlink[y]=predlink[x];

succlink[predlink[x]]=y;

predlink[x]=y; Включение элемента y после элемента x:

succlink[y]=succlink[x];

predlink[y]=x; predlink[succlink[x]]=y;

succlink[x]=y; Исключение элемента x:

predlink[succlink[x]]=predlink[x];

succlink[predlink[x]]=succlink[x];

Пример 1: /* В список помещаются цифры 1...10

Вводится число 11, сначала вставляется за цифрой 10,

затем рвется связь между 3 и 4, между ними вставляется число 11.

Пример дает навык работы:

- с динамической памятью;

- создания абстрактной структуры данных - список и

модификации списка;

- со структурами данных;

- с функциями. */

#include #include

#include #include

#include

#include struct List {

int i; List *next;

}; /* структура данных, первое поле для хранения целого, второе поле-адрес в динамической памяти*/

List*head=NULL; /* начальный адрес*/

void Hed(int i) /* функция, которая создает очередной элемент списка */

{ if(head==NULL) {

head=( List*)malloc(sizeof(List));

head->i=1;

head->next=NULL;

} else { struct List *p,*p1;

p=head; while(p->next!=NULL)

p=p->next;

p1=new List;

p1->i=i; p1->next=NULL;

p->next=p1;

}

} int s=0; void Print(List*p)/* вывод списка на экран */

{ printf(" %d",p->i);

if(p->next!=NULL)Print(p->next);

} void delist()/* освобождение динамической памяти */

{ List*p; while(head!=NULL) {

p=head; head=head->next;

free(p); }

} void Vstavka(int i1,int c)

/*вставка нового элемента*/

{ List*p=head,*p1;

while(p->i!=i1)

p=p->next;

p1=new List; p1->i=c;

p1->next=p->next;

p->next=p1; }

void main()/* вход в программу */

{ clrscr();/* очистить экран */

for(int i=1;i

#include #include

#include

#include #include

#define max_size 200

char s[max_size]; //компоненты стека

int next=0; // позиция стека

int Empty() { return next==0; }

int Full() { return next==max_size; }

void Push() { if (next==max_size)

{

cout> s[next-1];

} } void OUTst()

{ int i=0; if (next==0) {

cout> c;

clrscr(); switch (c) {

case '0':OUTst();getch();break;

case '1':Push();break;

case '2':Del();getch();break;

case '3':cout<<

"Hомер "<

case '4':if (Empty()==1) cout<<"Пуст"<

else cout<<"Hе пуст"<

case '5':if (Full()==1)cout<<"Полн"<

else cout<<"Hе полн"<

case '6':Clear();cout<<

"Стек очищен"<

case '7':exit(1);

} delay(200);

} while (c!=7);

return 0;

} Подведем итог

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

При проектировании программы попытайтесь предвидеть, что может идти неправильно, и программируйте, исходя из этого. Используйте локализацию ошибок, чтобы контролировать действия в местах потенциальных затруднений, или, по крайней мере, предупреждать пользователя, что может возникнуть осложнение. Гораздо лучше дать пользователю еще одну возможность ввести данные, чем продолжать выполнять программу и прийти к аварийной ситуации. Если создается функция, сначала определите, как она будет взаимодействовать с вызывающей программой. Решите также, какая информация будет входить в нее, а какая - выходить. Какими должны быть аргументы? Если вы примете во внимание все эти параметры, то можете обратить внимание на работу самой функции. Используйте эти идеи, и ваша программа будет более надежной и менее подверженной аварийным ситуациям. Вы получите тело функции, которое сможете применять в других программах. Программирование в таком случае потребует меньше времени. Не забывайте о классах памяти. Переменные можно определять вне функции. В этом случае их называют внешними или глобальными и они доступны более чем для одной функции. Переменные, определенные внутри функции, являются локальными для нее, и не известны другим функциям. Если можно, используйте автоматическую разновидность локальных переменных. Они охраняют переменные одной функции от взаимодействия других функций.

2

Показать полностью… https://vk.com/doc44823059_442236700
946 Кб, 17 февраля 2017 в 14:21 - Россия, Москва, СИЮ, 2017 г., doc
Рекомендуемые документы в приложении