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

Технология программирования

ТезисыПомощь в написанииУзнать стоимостьмоей работы

Целью преподавания дисциплины «Технология программирования» является изучение основ алгоритмизации задач, методов автоматизации программирования, классификации языков программирования, типов данных и классификации операторов языка, разработки программ с использованием функций, библиотечных встроенных функций, динамических структур данных, методов проектирования программного обеспечения, стиля… Читать ещё >

Технология программирования (реферат, курсовая, диплом, контрольная)

Тезисы лекционных материалов

Лекция 1.

Введение

Цели и задачи дисциплины «Технология программирования». Роль вычислительной техники в информационных системах. Компьютеризация учебного процесса. Общие сведения.

Введение

в систему программирования

Целью преподавания дисциплины «Технология программирования» является изучение основ алгоритмизации задач, методов автоматизации программирования, классификации языков программирования, типов данных и классификации операторов языка, разработки программ с использованием функций, библиотечных встроенных функций, динамических структур данных, методов проектирования программного обеспечения, стиля программирования, показателей качества программирования, методов отладки и испытания программ и основ объектно-ориентированного программирования. Уметь: грамотно выполнить постановку для решения любой задачи, разрабатывать структурные схемы различных алгоритмов; организовывать в зависимости от требований задачи необходимые структуры данных; разрабатывать программы на языке программирования с использованием средств языка и писать программы в хорошем стиле; отлаживать и испытывать программы, составлять качественную программную документацию.

Тема. Программные средства персонального компьютера (ПК). Методы автоматизации программирования. Назначение алгоритмического языка и требования, предъявляемые к нему. Понятие о процедурно-ориентированных языках и объектно-ориентированном программировании. Понятие о программном обеспечении ПК. Диалоговые средства связи пользователей с ПК. Интегрированные системы программирования.

Жизненный цикл ПО. Понятие жизненного цикла. Виды программ по длительности жизненного цикла. Этапы жизненного цикла программных средств, анализ (системный анализ и моделирование в определении требований к сложной системе), проектирование, реализация проекта, сопровождение и промышленная эксплуатация. Модели жизненного цикла ПО (каскадная и спиральная).

Определение технологии конструирования программного обеспечения Технология конструирования программного обеспечения (ТКПО) — система инженерных принципов для создания экономичного ПО, которое надежно и эффективно работает в реальных компьютерах [64], [69],.

Различают методы, средства и процедуры ТКПО.

Методы обеспечивают решение следующих задач:

* планирование и оценка проекта;

* анализ системных и программных требований;

* проектирование алгоритмов, структур данных и программных структур;

* кодирование;

* тестирование;

* сопровождение.

Основные этапы решения задачи на ЭВМ могут быть представлены следующими пунктами (рис. 1):

Методы автоматизации программирования. Различают методы, средства и процедуры ТКПО.

Методы обеспечивают решение следующих задач:

* планирование и оценка проекта;

* анализ системных и программных требований;

* проектирование алгоритмов, структур данных и программных структур;

* кодирование;

* тестирование;

* сопровождение.

Средства (утилиты) ТКПО обеспечивают автоматизированную или автоматическую поддержку методов. В целях совместного применения утилиты могут объединяться в системы автоматизированного конструирования ПО. Такие системы принято называть CASE-системами. Аббревиатура CASE расшифровывается как

Computer Aided Software Engineering (программная инженерия с компьютерной поддержкой).

Процедуры являются «клеем», который соединяет методы и утилиты так, что они обеспечивают непрерывную технологическую цепочку разработки. Процедуры определяют:

* порядок применения методов и утилит;

* формирование отчетов, форм по соответствующим требованиям;

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

* формирование «вех», по которым руководители оценивают прогресс.

Процесс конструирования программного обеспечения состоит из последовательности шагов, использующих методы, утилиты и процедуры. Эти последовательности шагов часто называют парадигмами ТКПО.

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

Рассмотрим наиболее популярные парадигмы ТКПО.

Классический жизненный цикл Старейшей парадигмой процесса разработки ПО является классический жизненный цикл (автор Уинстон Ройс, 1970) Очень часто классический жизненный цикл называют каскадной или водопадной моделью, подчеркивая, что разработка рассматривается как последовательность этапов, причем переход на следующий, иерархически нижний этап происходит только после полного завершения работ на текущем этапе (рис. 1.1).

Охарактеризуем содержание основных этапов.

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

Системный анализ задает роль каждого элемента в компьютерной системе, взаимодействие элементов друг с другом.

Рисунок -1.1.Классический жизненный цикл разработки ПО.

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

Анализ требований относится к программному элементу — программному обеспечению. Уточняются и детализируются его функции, характеристики и интерфейс.

Проектирование состоит в создании представлений:

* архитектуры ПО;

* модульной структуры ПО;

* алгоритмической структуры ПО;

* структуры данных;

* входного и выходного интерфейса (входных и выходных форм данных). Все определения документируются в спецификации анализа. Здесь же завершается решение задачи планирования проекта.

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

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

Тестирование — выполнение программы для выявления дефектов в функциях, логике и форме реализации программного продукта.

Сопровождение — это внесение изменений в эксплуатируемое ПО. Цели изменений:

* исправление ошибок;

* адаптация к изменениям внешней для ПО среды;

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

Сопровождение ПО состоит в повторном применении каждого из предшествующих шагов (этапов) жизненного цикла к существующей программе, но не в разработке новой программы.

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

Достоинства классического жизненного цикла: дает план и временной график по всем этапам проекта, упорядочивает ход конструирования.

Недостатки классического жизненного цикла:

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

цикл основан на точной формулировке исходных требований к ПО (реальное начале проекта требования заказчика определены лишь частично);

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

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

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

Основная цель макетирования — снять неопределенности в требованиях заказчика.

Макетирование (прототипирование) — это процесс создания модели требуемого программного продукта.

Модель может принимать одну из трех форм:

бумажный макет или макет на основе ПК (изображает или рисует человекомашинный диалог);

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

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

Как показано на рис. 1.2, макетирование основывается на многократном повторении итераций, в которых участвуют заказчик и разработчик.

Рисунок — 1.2 Макетирование Последовательность действий при макетировании представлена на рис. 1.3.

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

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

Рисунок — 1.3. Последовательность действий при макетировании

Быстрое проектирование приводит к построению макета.

Макет оценивается заказчиком и используется для уточнения требований к ПО.

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

Достоинство макетирования: обеспечивает определение полных требований к ПО.

Недостатки макетирования:

* заказчик может принять макет за продукт;

* разработчик может принять макет за продукт.

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

Спиральная модель Спиральная модель — классический пример применения эволюционной стратегии конструирования.

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

На рисунке 1.4. представлена спиральная модель.

Рисунок-1.4. Спиральная модель

7 — начальный сбор требований и планирование проекта; 2 — та же работа, но на основе рекомендаций заказчика; 3 — анализ риска на основе начальных требований; 4 — анализ риска на основе реакции заказчика; 5 — переход к комплексной системе; б — начальный макет системы; 7 — следующий уровень макета; 8 — сконструированная система; 9 — оценивание заказчиком Как показано на рис. 1.4, модель определяет четыре действия, представляемые четырьмя квадрантами спирали.

Планирование — определение целей, вариантов и ограничений.

Анализ риска — анализ вариантов и распознавание/выбор риска.

Конструирование — разработка продукта следующего уровня.

Оценивание — оценка заказчиком текущих результатов конструирования.

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

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

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

Достоинства спиральной модели:

наиболее реально (в виде эволюции) отображает разработку программного обеспечения;

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

включает шаг системного подхода в итерационную структуру разработки;

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

Недостатки спиральной модели:

новизна (отсутствует достаточная статистика эффективности модели);

повышенные требования к заказчику;

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

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

Литература

3 [10−100]

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

Какие этапы классического жизненного цикла вы знаете?

Охарактеризуйте содержание этапов классического жизненного цикла.

Объясните достоинства и недостатки классического жизненного цикла.

Чем отличается классический жизненный цикл от макетирования?

Какие существуют формы макетирования?

Чем отличаются друг от друга стратегии конструирования ПО?

Укажите сходства и различия классического жизненного цикла и инкрементной (макетирования) модели.

9.Объясните достоинства и недостатки инкрементной модели.

Чем отличается модель быстрой разработки приложений от инкрементной модели?

Объясните достоинства и недостатки модели быстрой разработки приложений.

Укажите сходства и различия спиральной модели и классического жизненно го цикла.

В чем состоит главная особенность спиральной модели?

Чем отличается компонентно-ориентированная модель от спиральной модели и классического жизненного цикла?

Перечислите достоинства и недостатки компонентно-ориентированной модели.

Лекция 2. Методология программирования. Этапы и уpовни pазpаботки пpогpамм. Техническое задание на pазpаботку пpогpамм. Этап технического пpоектиpования пpогpамм. Разpаботка стpуктуpных схем алгоpитмов. Оpганизация данных. Разpаботка стpуктуpы пpогpамм и внутpипpогpаммного интеpфейса Техническое задание. Включает наиболее полное и точное описание внешнего эффекта программы, т. е. что она должна делать, какие у нее исходные данные и результаты, ограничения на область применимости, ограничения на эффективность (если они есть) Разработка структурных схем алгоритмов Алгоритмизация. Анализ существующего или разработка нового алгоритма. При анализе существующего алгоритма (или нескольких) следует обращать внимание на: расчетные формулы; характеристики алгоритма по скорости, точности, требуемой памяти и области его применимости. Вывод расчетных формул при описании алгоритма можно опускать. Как правило, в математических книгах алгоритмы описываются в виде, не пригодном к непосредственному переводу на язык программирования.

Пример. Вычислить значение

y=ax3+bx2+cs+d

Прямое решение

Y=A*X**3+B*X**2+C*X+D

Более эффективное решение

Y=A*X*X*X +B*X*X +C*X+D (повторное умножение более эффективно, чем возведение в степень) Дальнейшее упрощение алгоритма — разложение полинома (методом Горнера):

y=ax3+bx2+cs+d=x (ax2+bx+c)+d=x (x (ax+b)+c)+d

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

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

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

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

Оптимизация. При высоких требованиях к эффективности программы по скорости и памяти на основе псевдокода принимаются решения по оптимизации программы: выделяются участки кода программы, которые целесообразно выделить в процедуры, выделяются циклы, по которым можно оценить наиболее медленные места программы. По структуре данных можно оценить необходимую память. Иначе говоря, на данном этапе имеются все необходимые данные для оценки параметров программы. Если программа не удовлетворяет требованиям эффективности, то следует вернуться к п. 5, 4 или даже 2. В крайнем случае, если выясняется невозможность удовлетворить требованиям технического задания, приходится возвращаться к п. 1.

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

Тесты. Разрабатываются по схеме и техническому заданию на программу. Определяя количество тестов, необходимо следить, чтобы проверялись все возможные варианты внешнего эффекта алгоритма и чтобы все ветви схемы были пройдены минимум один раз. Для каждого теста выписываются исходные данные, результаты и те данные, которые должны выдать отладочные средства. При составлении тестов могут быть обнаружены ошибки, для корректировки которых следует вернуться к п. 5 или 4, а далее повторно пройти этапы 5, 6, 7, так как изменения в схеме программы могут привести к иной расстановке отладочных средств и иному комплекту тестов.

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

Тестирование и отладка. Записанная на языке программирования программа выполняется с тестовыми исходными данными с целью обнаружения ошибок (тестирование). Если результаты выполнения расходятся с ожидаемыми, то принимаются меры к поиску причин расхождения (отладка). Для исправления ошибки необходимо перепрограммировать какую-то часть программы, т. е. вернуться к п. 4 или 5.

Счет. По завершении отладки из программы удаляются отладочные средства, программа записывается в оттранслированном виде на магнитные носители. Затем программа поступает в эксплуатацию.

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

Таковы основные этапы разработки программы.

Модели качества процессов конструирования В современных условиях, условиях жесткой конкуренции, очень важно гарантировать высокое качество вашего процесса конструирования ПО. Такую гарантию дает сертификат качества процесса, подтверждающий его соответствие принятым международным стандартам. Каждый такой стандарт фиксирует свою модель обеспечения качества. Наиболее авторитетны модели стандартов ISO 9001:2000, ISO/ IEC 15 504 и модель зрелости процесса конструирования ПО (Capability Maturity Model — СММ) Института программной инженерии при американском университете Карнеги-Меллон.

Модель стандарта ISO 9001:2000 ориентирована на процессы разработки из любых областей человеческой деятельности. Стандарт ISO/IEC 15 504 специализируется на процессах программной разработки и отличается более высоким уровнем детализации. Достаточно сказать, что объем этого стандарта превышает 500 страниц. Значительная часть идей ISO/IEC 15 504 взята из модели СММ.

Базовым понятием модели СММ считается зрелость компании [61],. Незрелой называют компанию, где процесс конструирования ПО и принимаемые решения зависят только от таланта конкретных разработчиков. Как следствие, здесь высока вероятность превышения бюджета или срыва сроков окончания проекта.

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

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

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

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

Для перехода на повторяемый уровень (уровень 2) необходимо внедрить формальные процедуры для выполнения основных элементов процесса конструирования. Результаты выполнения процесса соответствуют заданным требованиям и стандартам. Основное отличие от уровня 1 состоит в том, что выполнение процесса планируется и контролируется. Применяемые средства планирования и управления дают возможность повторения ранее достигнутых успехов.

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

С переходом на управляемый уровень (уровень 4) в компании принимаются количественные показатели качества как программных продуктов, так и процесса. Это обеспечивает более точное планирование проекта и контроль качества его результатов. Основное отличие от уровня 3 состоит в более объективной, количественной оценке продукта и процесса.

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

Каждый уровень СММ характеризуется областью ключевых процессов (ОКП), причем считается, что каждый последующий уровень включает в себя все характеристики предыдущих уровней. Иначе говоря, для 3-го уровня зрелости рассматриваются ОКП 3-го уровня, ОКП 2-го уровня и ОКП 1-го уровня. Область ключевых процессов образуют процессы, которые при совместном выполнении приводят к достижению определенного набора целей. Например, ОКП 5-го уровня образуют процессы:

Ш предотвращения дефектов;

Ш управления изменениями технологии;

Ш управления изменениями процесса.

Если все цели ОКП достигнуты, компании присваивается сертификат данного уровня зрелости.

Если хотя бы одна цель не достигнута, то компания не может соответствовать данному уровню СММ.

Литература

3 [10−100]

Контрольные вопросы

1.Назовите этапы алгоритма?

2.Разработка структурных схем, объясните.

3. Назовите модели качества создания прогрммы?

4. Назовите критерии для оценки зрелости компании?

Лекция 3. Основы технологии программирования. Методы пpоектиpования пpогpаммного обеспечения. Hисходящее и восходящее пpоектиpование пpогpамм и их сочетание. Стpуктуpное пpогpаммиpование. Модульное пpогpаммиpование. Выбоp языка пpогpаммиpования. Стиль пpогpаммиpования. Показатели качества пpогpаммиpования. Читаемость пpогpамм, комментаpии. Пpогpаммиpование с защитой от ошибок. Этап отладки и испытания пpогpамм Понятие технологичности программного обеспечения Под технологичностью понимают качество проекта программного продукта, от которого зависят трудовые и материальные затраты на его реализацию и последующие модификации. Хороший проект сравнительно быстро v легко кодируется, тестируется, отлаживается и модифицируется.

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

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

Для обеспечения необходимых технологических свойств применяются специальные технологические приемы и следуют определенным методикам, сформулированным всем предыдущим опытом создания программного обеспечения. К таким приемам и методикам относят правила декомпозиции, методы проектирования, программирования и контроля качества, которые под общим названием «структурный подход к программированию» были сформулированы еще в 60-х годах XX в.

В его основу были положены следующие основные концепции:

нисходящая разработка;

модульное программирование;

структурное программирование;

сквозной структурный контроль.

Нисходящая и восходящая разработка программного обеспечения При проектировании, реализации и тестировании компонентов структурной иерархии, полученной при декомпозиции, применяют два подхода:

— восходящий;

— нисходящий.

В литературе встречается еще один подход, получивший название «расширение ядра». Он предполагает, что в первую очередь проектируют и разрабатывают некоторую основу — ядро программного обеспечения, например, структуры данных и процедуры, связанные с ними. В дальнейшем ядро наращивают, комбинируя восходящий и нисходящий методы. На практике данный подход в зависимости от уровня ядра практически сводится либо к нисходящему, либо к восходящему подходам.

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

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

Подход имеет следующие недостатки:

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

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

— позднее проектирование интерфейса, а соответственно невозможности продемонстрировать его заказчику для уточнения спецификаций и т. д.

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

Нисходящий подход.

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

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

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

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

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

— достижимость модуля — наличие всех модулей в цепочке вызова данного модуля;

— зависимость по данным — модули, формирующие некоторые данные, должны создаваться раньше обрабатывающих;

— обеспечение возможности выдачи результатов — модули вывода результатов должны создаваться раньше обрабатывающих;

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

— наличие необходимых ресурсов.

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

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

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

Нисходящий подход обеспечивает:

— максимально полное определение спецификаций проектируемого компонента и согласованность компонентов между собой;

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

— возможность нисходящего тестирования и комплексной отладки (см гл. 9).

Структурное и «неструктурное» программирование. Средства описания структурных алгоритмов Одним из способов обеспечения высокого уровня технологичности разрабатываемого программного обеспечения является структурное программирование.

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

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

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

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

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

Именно для изображения схем алгоритмов таких программ в свое время был разработан ГОСТ 19.701−90, согласно которому каждой группе действий ставится в соответствие специальный блок (табл. 1 .1). Хотя этот стандарт предусматривает блоки для обозначения циклов, он не запрещает и произвольной передачи управления, т. е. допускает использование команд условной и безусловной передачи управления при реализации алгоритма.

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

— следование — обозначает последовательное выполнение действий (рис. 2.3, а);

— ветвление — соответствует выбору одного из двух вариантов действий (рис. 2.3, б);

— цикл-пока — определяет повторение действий, пока не будет нарушено некоторое условие, выполнение которого проверяется в начале цикла (рис. 2.3, в).

Рисунок 2.1-Базовые алгоритмические структуры:

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

— выбор — обозначает выбор одного варианта из нескольких в зависимости от значения некоторой величины (рис. 2.4, а);

— цикл-до — обозначает повторение некоторых действий до выполнения заданного условия, проверка которого осуществляется после выполнения действий в цикле (рис. 2.4, б);

— цикл с заданным числом повторений (счетный цикл) — обозначает повторение некоторых действий указанное количество раз (рис. 2.4, в).

Рисунок 2.2- Дополнительные структуры алгоритмов: авыбор, бцикл-до, вцикл с заданным числом повторений.

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

Примечание. Слово «структурное» в данном названии подчеркивает тот факт, что при программировании использованы только перечисленные конструкции (структуры). Отсюда и понятие «программирование без go to» .

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

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

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

процедурный (или структурный — по названию подхода);

объектный.

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

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

Термин «модуль» стал использоваться и в смысле автономно компилируемый набор программных ресурсов. Данные модуль может получать и/или возвращать через общие области памяти или параметры.

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

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

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

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

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

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

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

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

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

меньше вероятность появления новых ошибок при исправлении старых или внесении изменений в программу, т, е. вероятность появления «волнового» эффекта;

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

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

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

Различают пять типов сцепления модулей:

по данным;

по образцу;

по управлению;

по общей области данных;

по содержимому.

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

Например, функция Мах предполагает сцепление по данным через параметры скалярного типа:

Function Max (a, b: integer).'integer; begin

if a>b then Max:=a else Max:=b; end;

Сцепление по образцу предполагает, что модули обмениваются данными, объединенными в структуры. Этот тип также обеспечивает неплохие характеристики, но они хуже, чем у предыдущего типа, так как конкретные передаваемые данные «спрятаны» в структуры, и потому уменьшается «прозрачность» связи между модулями. Кроме того, при изменении структуры передаваемых данных необходимо модифицировать все использующие е модули.

Так, функция MaxEl, описанная ниже, предполагает сцепление по образу (параметр, а — открытый массив).

Function MaxEl (a:array of integer).'integer; Var i: word; begin

MaxEl: =a[OJ;

for i:=l to High (a) do

if a[i]>MaxEl then MaxEl:=afij;

end;

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

Например, функция MinMax предполагает сцепление по управлению так как значение параметра flag влияет на логику программы: если функция MinMax получает значение параметра flag, равное true, то возвращает максимальное значение из двух, а если false, то минимальное:

Function MinMax (a, b: integer;jlag:boolean):integer; begin

if (a>b) and (flag) then MinMax: =a

else MinMax: =6; end;

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

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

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

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

Например, функция МахА, использующая глобальный массив А, сцеплена с основной программой по общей области:

информационный компьютеризация программа файл

Function MaxA: integer; Var i: word; begin

МахА: =a[Low (a)J; for i:= Low (a)+l to High (a) do if a[i]>MaxA then MaxA:=a[i];

end;

Следует иметь в виду, что «подпрограммы с памятью», действия которых зависят от истории вызовов, используют сцепление по общей области, что делает их работу в общем случае непредсказуемой. Именно этот вариант используют статические переменные С и C++.

В случае сцепления по содержимому один модуль содержит обращения к внутренним компонентам другого (передает управление внутрь, читает и/или изменяет внутренние данные или сами коды), что полностью противоречит блочно-иерархическому подходу. Отдельный модуль в этом случае уже не является блоком («черным ящиком»): его содержимое должно учитываться в процессе разработки другого модуля. Современные универсальные языки процедурного программирования, например Pascal, данного типа сцепления в явном виде не поддерживают, но для языков низкого уровня, например Ассемблера, такой вид сцепления остается возможным.

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

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

последовательную;

информационную (коммуникативную);

процедурную;

временную;

логическую;

случайную.

При функциональной связности все объекты модуля предназначены выполнения одной функции (рис. 2.1, а): операции, объединяемые для выполнения одной функции, или данные, связанные с одной функцией. Модуль элементы которого связаны функционально, имеет четко определенную цель, при его вызове выполняется одна задача, например, подпрограмма поиска минимального элемента массива. Такой модуль имеет максимальную связность, следствием которой являются его хорошие технологические качества: простота тестирования, модификации к сопровождения. Именно с этим связано одно из требований структурной декомпозиции «один модуль — с функция» .

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

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

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

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

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

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

В качестве средства улучшения технологических характеристик библиотек ресурсов в настоящее время широко используют разделение тела модуля на интерфейсную часть и область реализации (секции Interface и Implementation — в Pascal, h и срр-файлы в C++ и в Java).

Интерфейсная часть в данном случае содержит совокупность объявлений ресурсов (заголовков подпрограмм, имен переменных, типов, классов и т. п.), которые данная библиотека предоставляет другим модулям. Ресурсы, объявление которых в интерфейсной части отсутствует, извне не доступны. Область реализации содержит тела подпрограмм и, возможно, внутренние ресурсы (подпрограммы, переменные, типы), используемые этими подпрограммами. При такой организации любые изменения реализации библиотеки, не затрагивающие ее интерфейс, не требуют пересмотра модулей, связанных с библиотекой, что улучшает технологические характеристики модулей-библиотек. Кроме того, подобные библиотеки, как правило, хорошо отлажены и продуманы, так как часто используются разными программами Документиpование пpогpамм. Составление программной документации — очень важный процесс.

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

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

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

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

Литература

Контрольные вопросы

1. Назовите три основных вида модулей?

2. Что означает Библиотеки ресурсов?

3. Что такое модули и приведите их свойства?

4. Назовите виды структурного программирования?

Лекция 4. Выбоp языка пpогpаммиpования. Стиль пpогpаммиpования. Показатели качества пpогpаммиpования. Читаемость пpогpамм, комментаpии. Пpогpаммиpование с защитой от ошибок. Этап отладки и испытания пpогpамм. Документиpование пpогpамм. Виды пpогpаммной документации, установленные ГОСТом. Единая система пpогpаммной документации (ЕСПД)

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

Программирование на языках высокого уровня;

Программирование с защитой от ошибок;

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

Программирование в стандартизованном стиле;

Нисходящее программирование.

Выбоp языка пpогpаммиpования.

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

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

Положительные характеристики ЯВУ, позволяющие уменьшить количество ошибок:

Любая функция обработки данных на ЯВУ выражается меньшим числом операторов, чем на языке низкого уровня (ЯНУ);

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

ЯВУ отличается большей наглядностью, которая обеспечивается, с одной стороны, соответствующим составом ключевых слов, а с другой — возможностью модульного построения программ. Наглядность полезна по причинам: 1) упрощается документирование программ; 2) быстрее выявляются ошибки; 3) программа значительно удобнее для сопровождения другим программистом.

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

Эффективность программ, реализованных на ЯВУ несколько ниже, чем у программ на ЯНУ. Однако снижающаяся стоимость технических средств позволяет в большинстве случаев считать необходимость достижения максимальной эффективности программ не главной целью при создании ПИ. Основные резервы повышения эффективности программ, как показывает практика, лежат в области разумного выбора методов и алгоритмов. В программах имеется только от 5 до 20% модулей, совершенствование которых (в том числе и возможное перепрограммирование на ЯНУ) может обеспечить повышение общей эффективности программы на 80%.

Программирование с защитой от ошибок.

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

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

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

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

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

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

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

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

Программирование в стандартизованном стиле.

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

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

Основные принципы стандартизации стиля программирования следующие.

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

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

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

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

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

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

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

Правила выбора имен. Большую роль при программировании имеет правильное присваивание имен данным, файлам, процедурам и функциям. Имена должны обладать мнемоничностью, т. е. отражать сущность описываемых с их помощью объектов. При присваивании имен переменным можно пользоваться следующими правилами сокращения имен: сокращению подлежат не более трех первых букв; в аббревиатуру всегда включаются начальные буквы слов; согласные буквы важнее гласных; начало слова всегда важнее его конца; аббревиатура должна включать от 6 до 15 символов.

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

Использовать дополнительные пробелы для выделения составных частей операторов

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

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

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

Критерии качества программы.

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

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

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

правильность (корректность);

точность;

совместимость надежность;

универсальность;

защищенность;

полезность эффективность;

проверяемость;

адаптируемость.

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

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

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

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

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

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

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

Программа является эффективной, если объем требуемых для ее работы ресурсов ЭВМ не превышает допустимого предела.

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

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

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

Типы ошибок Тип 1. Ошибки в программном комплексе, допущенные при разработке и не обнаружении его при тестировании.

Тип 2. Ошибки, возникающие при вводе в компьютер неверных данных.

Тип 3. Компьютерные вирусы Тип 4. Выход из строя элементов компьютера и обслуживающих его систем.

Тип 5. Злая воля человека. Программист вносит ошибку.

Виды программных документов.

К программным относят документы, содержащие сведения, необходимые для разработки, сопровождения и эксплуатации программного обеспечения. Документирование программного обеспечения осуществляется в соответствии с Единой системой программной документации (ГОСТ 19. ХХХ). Так ГОСТ 19.101−77 устанавливает виды программных документов для программного обеспечения различных типов. Ниже перечислены основные программные документы по этому стандарту и указано, какую информацию они должны содержать.

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

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

Текст программы (код вида документа — 12) должен содержать текст программы с необходимыми комментариями. Необходимость этого документа определяется на этапе разработки и утверждения технического задания.

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

Ведомость эксплуатационных документов (код вида документа — 20) должна содержать перечень эксплуатационных документов на программу, к которым относятся документы с кодами: 30, 31, 32, 33, 34, 35,46. Необходимость этого документа также определяется на этапе разработки и утверждения технического задания.

Формуляр (код вида документа — 30) должен содержать основные характеристики программного обеспечения, комплектность и сведения об эксплуатации программы.

Описание применения (код вида документа — 31) должно содержать сведения о назначении программного обеспечения, области применения, применяемых методах, классе решаемых задач, ограничениях для применения, минимальной конфигурации технических средств.

Руководство системного программиста (код вида документа — 32) должно содержать сведения для проверки, обеспечения функционирования и настройки программы на условия конкретного применения.

Руководство программиста (код вида документа — 33) должно содержать сведения для эксплуатации программного обеспечения.

Руководство оператора (код вида документа — 34) должно содержать сведения для обеспечения процедуры общения оператора с вычислительной системой в процессе выполнения программного обеспечения.

Описание языка (код вида документа — 35) должно содержать описание синтаксиса и семантики языка.

Руководство по техническому обслуживанию (код вида документа — 46) должно содержать сведения для применения тестовых и диагностических программ при обслуживании технических средств.

Программа и методика испытаний (код вида документа — 51) должны содержать требования, подлежащие проверке при испытании программного обеспечения, а также порядок и методы их контроля.

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

Прочие документы (коды вида документа — 90−99) могут составляться на любых стадиях разработки, т. е. на стадиях эскизного, технического и рабочего проектов.

Допускается объединять отдельные виды эксплуатационных документов, кроме формуляра и ведомости. Необходимость объединения указывается в техническом задании, а имя берут у одного из объединяемых документов. Например, в настоящее время часто используется эксплуатационный документ, в который отчасти входит руководство системного программиста, программиста и оператора. Он называется «Руководство пользователя» (см. § 11.3).

Рассмотрим наиболее важные программные документы более подробно.

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

Содержание пояснительной записки по стандарту (ГОСТ 19.404−79) должно выглядеть следующим образом:

введение

;

назначение и область применения;

технические характеристики;

ожидаемые технико-экономические показатели;

источники, используемые при разработке.

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

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

Раздел Технические характеристики должен содержать следующие подразделы:

— постановка задачи, описание применяемых математических методов и допущений и ограничений, связанных с выбранным математическим аппаратом;

— описание алгоритмов и функционирования программы с обоснованием принятых решений;

— описание и обоснование выбора способа организации входных и выходных данных;

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

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

В разделе Источники, использованные при разработке, указывают перечень научно-технических публикаций, нормативно-технических документов и других научно-технических материалов, на которые есть ссылки в исходном тексте.

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

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

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

В книге С.Дж. Гримм даны рекомендации по написанию подобной программной документации:

учитывайте интересы пользователей — руководство должно содержать все инструкции, необходимые пользователю;

излагайте ясно, используйте короткие предложения;

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

будьте точны и рациональны — длинные и запутанные руководства обычно никто не читает, например, лучше привести рисунок формы, чем долго ее описывать.

Руководство пользователя, как правило, содержит следующие разделы:

общие сведения о программном продукте;

описание установки;

описание запуска;

инструкции по работе (или описание пользовательского интерфейса);

сообщения пользователю.

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

Раздел Установка обычно содержит подробное описание действий по установке программного продукта и сообщений, которые при этом могут быть получены.

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

Раздел Инструкции по работе обычно содержит описание режимов работы, форматов ввода-вывода информации и возможных настроек.

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

Руководство системного программиста По ГОСТ 19.503−79 руководство системного программиста должно содержать всю информацию, необходимую для установки программного обеспечения, его настройки и проверки работоспособности. Кроме того, как указывалось выше, в него часто включают и описание необходимого обслуживания, которое раньше приводилось в руководстве оператора (ГОСТ 19.505−79) и/или руководстве по техническому обслуживанию (ГОСТ 19.508−79). В настоящее время данную схему используют для составления руководства системному администратору.

Руководство системного программиста должно содержать следующие разделы:

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

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

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

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

В разделе Проверка программы должно быть приведено описание способов проверки работоспособности программы, например контрольные примеры.

В разделе Дополнительные возможности должно быть приведено описание дополнительных возможностей программы и способов доступа к ним.

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

Литература

Контрольные вопросы.

1. Какие Вы знаета основные методы программирования?

2. В чем значение выбор языка?

3. Что означает программирование с защитой от ошибок?

4. Что означает программирование в стандартизованном стиле?

Лекция 5. Программирование на языке СИ.

Введение

в систему программирования СИ. Директивы препроцессора. Состав системы программирования, элементы языка. Типы данных: int, short, long, unsigned, float, double. Объявления. Выражения и присваивания. Операции языка СИ

Директивы Препроцессора

Директивы препроцессора представляют собой инструкции, записанные в тексте программы на СИ, и выполняемые до трансляции программы. Директивы препроцессора позволяют изменить текст программы, например, заменить некоторые лексемы в тексте, вставить текст из другого файла, запретить трансляцию части текста и т. п. Все директивы препроцессора начинаются со знака #. После директив препроцессора точка с запятой не ставятся.

Директива #include

Директива #include включает в текст программы содержимое указанного файла. Эта директива имеет две формы:

#include «имя файла»

#include <�имя файла>

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

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

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

Директива #define

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

Директива #define имеет две синтаксические формы:

#define идентификатор текст

#define идентификатор (список параметров) текст Эта директива заменяет все последующие вхождения идентификатора на текст. Такой процесс называется макроподстановкой. Текст может представлять собой любой фрагмент программы на СИ, а также может и отсутствовать.

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

Пример:

#define WIDTH 80

#define LENGTH (WIDTH+10)

Эти директивы изменят в тексте программы каждое слово WIDTH на число 80, а каждое слово LENGTH на выражение (80+10) вместе с окружающими его скобками.

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

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

Пример:

#define MAX (x, y) ((x)>(y))?(x):(y)

Эта директива заменит фрагмент

t=MAX (i, s[i]);

на фрагмент

t=((i)>(s[i])?(i):(s[i]);

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

Например, при наличии скобок фрагмент

t=MAX (i&j, s[i]||j);

будет заменен на фрагмент

t=((i&j)>(s[i]||j)?(i&j):(s[i]||j);

а при отсутствии скобок — на фрагмент

t=(i&j>s[i]||j)?i&j:s[i]||j;

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

Директива #undef

Директива #undef используется для отмены действия директивы #define. Синтаксис этой директивы следующий #undef идентификатор

Директива отменяет действие текущего определения #define для указанного идентификатора. Не является ошибкой использование директивы #undef для идентификатора, который не был определен директивой #define.

Пример:

#undef WIDTH

#undef MAX

Эти директивы отменяют определение именованной константы WIDTH и макроопределения MAX.

Элементы Языка СИ

Используемые символы

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

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

Таблица 1

Прописные буквы латинского алфавита

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

Строчные буквы латинского алфавита

a b c d e f g h i j k l m n o p q r s t u v w x y z

Символ подчеркивания

_

Группа прописных и строчных букв русского алфавита и арабские цифры (табл.2).

Таблица 2

Прописные буквы русского алфавита

А Б В Г Д Е Ж З И К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ы Ь Э Ю Я

Строчные буквы русского алфавита

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

Арабские цифры

0 1 2 3 4 5 6 7 8 9

3. Знаки нумерации и специальные символы (табл. 3). Эти символы используются с одной стороны для организации процесса вычислений, а с другой — для передачи компилятору определенного набора инструкций.

Таблица 2

Символ

Наименование

Символ

Наименование

запятая

)

круглая скобка правая

.

точка

(

круглая скобка левая

;

точка с запятой

}

фигурная скобка правая

:

двоеточие

{

фигурная скобка левая

вопросительный знак

<

меньше

'

апостроф

>

больше

!

восклицательный знак

[

квадратная скобка

|

вертикальная черта

]

квадратная скобка

дробная черта

#

номер

обратная черта

%

процент

~

тильда

&

амперсанд

*

звездочка

^

логическое не

плюс

=

равно

;

мину

"

кавычки

4. Управляющие и разделительные символы. К той группе символов относятся: пробел, символы табуляции, перевода строки, возврата каретки, новая страница и новая строка. Эти символы отделяют друг от друга объекты, определяемые пользователем, к которым относятся константы и идентификаторы. Последовательность разделительных символов рассматривается компилятором как один символ (последовательность пробелов).

5. Кроме выделенных групп символов в языке СИ широко используются так называемые, управляющие последовательности, т. е. специальные символьные комбинации, используемые в функциях ввода и вывода информации. Управляющая последовательность строится на основе использования обратной дробной черты () (обязательный первый символ) и комбинацией латинских букв и цифр (табл.4).

Таблица 4

Управляющая последовательность

Наименование

Шеснадцатеричная замена

a

Звонок

b

Возврат на шаг

t

Горизонтальная табуляция

n

Переход на новую строку

00A

v

Вертикальная табуляция

00B

r

Возврат каретки

00C

f

Перевод формата

00D

"

Кавычки

'

Апостроф

Ноль-символ

\

Обратная дробная черта

05C

ddd

Символ набора кодов ПЭВМ в восьмеричном представлении

xddd

Символ набора кодов ПЭВМ в шестнадцатеричном представлении

Последовательности вида ddd и xddd (здесь d обозначает цифру) позволяет представить символ из набора кодов ПЭВМ как последовательность восьмеричных или шестнадцатеричных цифр соответственно.

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

r — общая управляющая последовательность,

15 — восьмеричная управляющая последовательность,

x00D — шестнадцатеричная управляющая последовательность.

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

Например:

" ABCDEx009FGH" данная строковая команда будет напечатана с использованием определенных функций языка СИ, как два слова ABCDE FGH, разделенные 8-ю пробелами, в этом случае если указать неполную управляющую строку" ABCDEx09FGH", то на печати появится ABCDE=|=GH, так как компилятор воспримет последовательность x09 °F как символ «=+=».

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

Например: символ h представляется символом h в строковой или символьной константе.

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

Константы

Константами называются перечисление величин в программе. В языке СИ разделяют четыре типа констант: целые константы, константы с плавающей запятой, символьные константы и строковыми литералы.

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

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

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

Шестнадцатеричная константа начинается с обязательной последовательности 0х или 0Х и содержит одну или несколько шестнадцатеричных цифр (цифры представляющие собой набор цифр шеснадцатеричной системы счисления: 0,1,2,3,4,5,6,7,8,9,A, B, C, D, E, F)

Примеры целых констант:

Если требуется сформировать отрицательную целую константу, то используют знак «-» перед записью константы (который будет называться унарным минусом). Например: -0x2A, -088, -16.

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

— десятичные константы рассматриваются как величины со знаком, и им присваивается тип int (целая) или long (длинная целая) в соответствии со значением константы. Если константа меньше 32 768, то ей присваивается тип int в противном случае long.

— восьмеричным и шестнадцатеричным константам присваивается тип int, unsigned int (беззнаковая целая), long или unsigned long в зависимости от значения константы согласно табл 5.

Таблица 5

Диапазон шестнадцатеричных констант

Диапазон восьмеричных констант

Тип

0x0 — 0x7FFF

0 — 77 777

int

0X8000 — 0XFFFF

100 000 — 177 777

unsigned int

0X10000 — 0X7FFFFFFF

200 000 — 17 777 777 777

long

0X80000000 — 0XFFFFFFFF

20 000 000 000 — 37 777 777 777

unsigned long

Для того чтобы любую целую константу определить типом long, достаточно в конце константы поставить букву «l» или «L». Пример:

5l, 6l, 128L, 0105L, OX2A11L.

Константа с плавающей точкой — десятичное число, представленное в виде действительной величины с десятичной точкой или экспонентой. Формат имеет вид:

[цифры]. цифры] [Е|e [+|-] цифры].

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

Примеры:

115.75, 1.5Е-2, -0.025, .075, -0.85Е2

Символьная константа — представляется символом заключенном в апострофы. Управляющая последовательность рассматривается как одиночный символ, допустимо ее использовать в символьных константах. Значением символьной константы является числовой код символа. Примеры:

' '- пробел,

'Q'- буква Q ,

'n' - символ новой строки,

'\' - обратная дробная черта,

'v' - вертикальная табуляция.

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

Строковая константа (литерал) — последовательность символов (включая строковые и прописные буквы русского и латинского, а также цифры) заключенные в кавычки («). Например: «Школа N 35», «город Тамбов», «YZPT КОД» .

Отметим, что все управляющие символы, кавычка («), обратная дробная черта () и символ новой строки в строковом литерале и в символьной константе представляются соответствующими управляющими последовательностями. Каждая управляющая последовательность представляется как один символ. Например, при печати литерала «Школа n N 35» его часть «Школа» будет напечатана на одной строке, а вторая часть «N 35» на следующей строке.

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

Строковый литерал имеет тип char[]. Это означает, что строка рассматривается как массив символов. Отметим важную особенность, число элементов массива равно числу символов в строке плюс 1, так как нулевой символ (символ конца строки) также является элементом массива. Все строковые литералы рассматриваются компилятором как различные объекты. Строковые литералы могут располагаться на нескольких строках. Такие литералы формируются на основе использования обратной дробной черты и клавиши ввод. Обратная черта с символом новой строки игнорируется компилятором, что приводит к тому, что следующая строка является продолжением предыдущей. Например:

" строка неопределенной n

длины"

полностью идентична литералу

" строка неопределенной длинны" .

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

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

Идентификатором называется последовательность цифр и букв, а также специальных символов, при условии, что первой стоит буква или специальный символ. Для образования идентификаторов могут быть использованы строчные или прописные буквы латинского алфавита. В качестве специального символа может использоваться символ подчеркивание (_). Два идентификатора для образования которых используются совпадающие строчные и прописные буквы, считаются различными. Например: abc, ABC, A128B, a128b .

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

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

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

Ключевые слова

Ключевые слова — это зарезервированные идентификаторы, которые наделены определенным смыслом. Их можно использовать только в соответствии со значением известным компилятору языка СИ.

Приведем список ключевых слов

auto double int struct break else long switch

register tupedef char extern return void case float

unsigned default for signed union do if sizeof

volatile continue enum short while

Кроме того в рассматриваемой версии реализации языка СИ, зарезервированными словами являются:

_asm, fortran, near, far, cdecl, huge, paskal, interrupt.

Ключевые слова far, huge, near позволяют определить размеры указателей на области памяти. Ключевые слова _asm, cdelc, fortran, pascal служат для организации связи с функциями написанными на других языках.

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

Использование комментариев в тексте программы

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

Отметим, что комментарии могут заменить как одну строку, так и несколько.

Например:

/* комментарии к программе */

/* начало алгоритма */

Или

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

Неправильное определение комментариев.

/* комментарии к алгоритму /* решение краевой задачи */ */

или

/* комментарии к алгоритму решения */ краевой задачи */

Концепция типа данных

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

Тип данных определяет:

· внутреннее представление данных в памяти компьютера;

· множество значений, которые могут принимать величины этого типа;

· операции и функции, которые можно применять к величинам этого типа.

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

Все типы языка С++ можно разделить на простые (скалярные), составные (агрегатные) и функциональные. Простые типы могут быть стандартными и определенными программистом.

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

Простые типы данных

Простые типы делятся на целочисленные типы и типы с плавающей точкой. Для описания стандартных типов определены следующие ключевые слова:

· int (целый);

· char (символьный);

· wchar_t (расширенный символьный);

· bool (логический);

· float (вещественный);

· double (вещественный с двойной точностью).

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

· short (короткий);

· long (длинный);

· signed (со знаком);

· unsigned (без знака).

Целый тип (int)

Размер типа int стандартом ANSI не определяется. Он зависит от реализации. Для 16-разрядного процессора под величины этого типа отводится 2 байта, для 32-разрядного — 4 байта.

Спецификатор short перед именем типа указывает компилятору, что под число требуется отвести 2 байта. Спецификатор long означает, что целая величина будет занимать 4 байта.

Внутреннее представление величины целого типа — целое число в двоичном коде. При использовании спецификатора signed старший бит числа интерпретируется как знаковый (0 — положительное число, 1 — отрицательное). Спецификатор unsigned позволяет представлять только положительные числа. Диапазоны значений величин целого типа с различными спецификаторами для IBM PC-совместимых компьютеров приведены в таблице 1.4.

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

Константам, встречающимся в программе, приписывается тип в соответствии с их видом. Программист может явно указать требуемый тип с помощью суффиксов L, l (long) и U, u (unsigned). Например, константа 32L имеет тип long и занимает 4 байта.

Примечание Типы shortint, longint, signedint иunsignedint можно сокращать до short, long, signed иunsignedсоответственно.

Символьный тип (char)

Под величину символьного типа отводится количество байт, достаточное для размещения любого символа из набора символов для данного компьютера. Как правило, это 1 байт.

Тип char, как и другие целые типы, может быть со знаком или без знака.

В величинах со знаком можно хранить значения в диапазоне от -128 до 127. При использовании спецификатора unsignedзначения могут находиться в пределах от 0 до 255. Величины типа char применяются также для хранения целых чисел, не превышающих границы указанных диапазонов.

Расширенный символьный тип (wchar_t)

Тип wchar_t предназначен для работы с набором символов, для кодировки которых недостаточно 1 байта, например, Unicode. Размер этого типа зависит от реализации; как правило, он соответствует типу short.

Логический тип (bool)

Величины логического типа могут принимать только значения true и false. Внутренняя форма представления значения false — 0 (нуль). Любое другое значение интерпретируется как true. При преобразовании к целому типу true имеет значение 1.

Типы с плавающей точкой (float, double и longdouble)

Стандарт С++ определяет три типа данных для хранения вещественных значений: float, double и longdouble.

Внутреннее представление вещественного числа состоит из мантиссы и порядка. Длина мантиссы определяет точность числа, а длина порядка — его диапазон.

Константы с плавающей точкой имеют по умолчанию тип double. Можно явно указать тип константы с помощью суффиксов F, f (float) и L, l (long). Например, константа 2E+6L будет иметь типlongdouble.

Таблица 1.3. Диапазоны значений простых типов данных для IBM PC

Тип

Диапазон значений

Размер (байт)

Bool

true и false

signed char

— 128 … 127

Unsigned char

0 … 255

signed short int

— 32 768 … 32 767

Unsigned short int

0 … 65 535

signed long int

— 2 147 483 648 … 2 147 483 647

Unsigned long int

0 … 4 294 967 295

Float

3.4e-38 … 3.4e+38

Double

1.7e-308 … 1.7e+308

long double

3.4e-4932 … 3.4e+4932

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

Тип void

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

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

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

Си — программа, печатающая «Привет!», выглядит так:

#include

main ()

{ printf («Привет!n»);}

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

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

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

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

Переменные и арифметические выражения.

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

В Си любая переменная должна быть описана раньше чем она будет использована, т. е. перед первой исполняемой инструкцией. Базовые типы для данных: char, short, long, double, int, float. Из базовых типов можно создавать: массивы, структуры и объединения, указатели на объекты базовых типов и функции, возвращающие в качестве результата значения этих типов.

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

Рассмотрим программу вычисления температур по Фаренгейту и по Цельсию.

# include

/ * печать таблицы температур по Фаренгейту и Цельсию для значений 0,20, …, 300 */

main ()

{ float f, c;

int low, up, step;

low=0; /* нижняя граница таблицы температур */

up=300; // верхняя граница

step=20; // шаг

f=low;

while (f<=up)

{ c=(5.0 / 9.0)*(f-32.0);

printf («%3.0f %6.1 fn», f, c);

f=f+step;

}

}

Большая часть вычислений выполняется в теле цикла while.

Стандартная функция printf — универсальная функция форматного ввода-вывода. Ее первый аргумент-стринг, в котором каждый знак % соответствует одному из последующих ее аргументов, а информация, расположенная за знаком %, указывает на вид, в котором каждый из этих аргументов выводится.

Например, %d специфицирует выдачу аргумента в виде целого числа. Спецификация %3.0f в printf определяет печать числа типа float с плавающей точкой в поле шириной не более трех позиций без десятичной точки и дробной части. Спецификация %6.1f описывает печать числа в поле из шести позиций с одной цифрой после десятичной точки. Спецификатор %.0f означает печать без десятичной точки и дробной части. Напечатано будет следующее:

0 -17.8

20 -6.7

40 4.4

Ширину и точность можно не задавать: %6f означает, что число будет занимать не более шести позиций; %.2f — число имеет две цифры после десятичной точки, но ширина не ограничена; %f просто указывает на печать числа с плавающей точкой; %6d — печать десятичного целого в поле из 6 позиций.

Кроме того, printf допускает также следующие спецификаторы: %0 — для восьмеричного числа, %X — для шестнадцатеричного числа, %С — для печати литеры, %S — для печати стринга литер %% - для самого %.

Другой способ написания программы преобразования температур:

# include // печатать таблицы температур по Фаренгейту и Цельсию

main ()

{ int f;

for (f=0; f<=300; f=f+20)

printf («%3d %6.1fn», f, (5.0/9.0)*(f-32));

}

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

Это последнее изменение являются примером применения общего правила: в любом контексте, где возможно использование значения переменной какого-то типа, можно использовать более сложное выражение того же типа. Так на, месте третьего аргумента функции printf согласно спецификатору %6.1f должно быть значение с плавающей точкой, следовательно, здесь может быть любое выражение этого типа.

Именованные константы, ввод-вывод литер

Именованные константы

Очень важно в программировании давать четкие представления используемым данным. Одна из возможностей — давать числам осмысленные имена. Строка, начинающаяся со знака #, устанавливает связь с препроцессором, который выполняет макроподстановку, условную компиляцию, включение именованных файлов. Синтаксис такой строки не зависит от остальной части языка Си; такая строка может появляться где угодно в тексте программы и оказывать влияние (независимо от области действия) вплоть до конца транслируемой компоненты.

Строка #define определяет символьное имя, или именованную константу, для заданного стринга литер.

#define имя подставляемый_текст

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

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

# include

# define LOWER 0 // нижняя граница таблицы

# define UPPER 300 // верхняя граница

# define STEP 20 // размер шага

/* печать таблицы температур по Фаренгейту и Цельсию */

main ()

{ int f;

for (f=LOWER; f<=UPPER; f=f+STEP)

printf («%3d %6.1fn», f, (5.0/9.0)*(f-32));

}

Ввод-вывод литер.

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

Простейшими из функций чтения и записи одной литеры являются — getchar и putchar, соответственно чтение следующей литеры ввода из текстового потока и печать содержимого целой переменной в виде литеры.

Например, программа копирования по одной литере с входного потока в выходной поток:

# include // копирование ввода на вывод

main ()

{ int с;

c=getchar ();

while (c≠EOF) // константа EOF, определенная в

{ putchar (c);

с=getchar ();

}

}

Для хранения литерных данных специально предназначен тип char, однако для этого также годится и любой целый тип.

Использование типа int, а не char для переменной с связано с одной важной причиной: функция getchar по исчерпании входного потока в качестве своего результата должна выдавать значение, отличное от любой реальной литеры.

В Си любое присваивание, и в частности c=getchar (), трактуется как выражение со значением, равным значению левой части после присваивания. Это значит, что присваивание может встречаться внутри более сложного выражения.

# include // копирование ввода на вывод

main ()

{ int с;

while ((c=getchar ())≠EOF) // присваивание вместе с проверкой

putchar (c);

}

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

# include // подсчет литер

main ()

{ double nc;

for (nc=0; getchar ()≠EOF; ++nc); // пустой оператор тела цикла

printf («%.0f n», nc);

}

# include // подсчет строк

main ()

{ int c, nline=0;

while ((c=getchar ())≠EOF)

if (c= ='n') ++nline;

printf («%dn», nline);

}

# include

# define IN 1 // внутри слова

# define OUT 0 // вне слова

// подсчет строк, слов и литер

main ()

{ int c, nline, nword, nchar, state;

state=OUT;

nline=nword=nchar=0;

while ((c=getchar ())≠EOF)

{ ++nchar;

if (c= = `n') ++nline;

if (c= =' ` ¦ ¦ c = = `n' ¦ ¦ c= ='t') state=OUT;

else if (state = = OUT)

{ state = IN;

++nword;

}

}

printf («%d%d%dn», nline, nword, nchar);

}

Описание. Массивы, функции

Декларация int array[10]; определяет array как массив из 10 значений типа int. В Си элементы массива всегда нумеруются, начиная с нуля. Индексом может быть любое целое выражение, образуемое целыми переменными и целыми константами.

Напишем, программу, подсчитывающую по отдельности каждую цифру, пробельные литеры (пробелы ` ', табуляции `t' и новые строки — `n') и все другие литеры. Счетчик цифр храним в массиве.

# include /* подсчет цифр, пробельных и прочих литер */

main ()

{ int c, i, nwhite, nother;

int ndigit [10];

nwhite=nother=0;

for (i=0; i<10; i++) ndigit [i]=0;

while ((c=getchar ())≠EOF)

if (c>='0' && c<='9') // является ли литера цифрой?

++ndigit [c-`0'];

else if (c = = ` `¦ ¦ c = = `n' ¦ ¦ c = = `t') ++nwhite;

else ++nother;

printf («цифры = «);

for (i=0; i<10; i++)

printf (" %d", ndigit [i]);

printf («, пробелы=%d, прочие =%dn», nwhite, nother);

}

Проверка if (c>='0' && c<='9') определяет, является ли, находящаяся в с литера цифрой. Если это так, то c-`0' есть числовое значение цифры.

По определению значения типа char являются всего лишь малыми целыми, так что переменные и константы типа char в арифметических выражениях идентичны значениям типа int. Например, c-`0' - есть целое выражение с возможными значениями от 0 до 9, которые соответствуют литерам от `0' до `9', хранящимся в переменной с. Таким образом, значение данного выражения является правильным индексом для массива ndigit.

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

Определение любой функции имеет следующий вид:

тип_результата имя_функции (список_формальных_параметров)

{ декларации

инструкции

}

Кроме определения функции существует понятие прототипа функции, формат которого имеет следующий вид:

тип_результата имя_функции (список_формальных_параметров);

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

Имена параметров не требуют согласования. Фактически в прототипе они могут быть произвольными или вообще отсутствовать.

Значение, вычисляемое функцией, возвращается в main с помощью инструкции return. За словом return может следовать любое выражение:

return выражение;

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

Поскольку main есть функция, как и любая другая, она может вернуть результирующее значение тому, кто ее вызвал, — фактически в операционную среду, в которой была запущена программа. Обычно возвращается нулевое значение, что говорит о нормальном завершении счета (return 0;).

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

Аргументы, вызов по значению

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

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

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

Если аргументом функции является массив (имя массива), то функции передается значение, которое является адресом начало этого массива; никакие элементы массива не копируются.

Массивы литер

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

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

Схема программы нашего примеры достаточно проста:

ЦИКЛ-ПОКА (есть ли еще строка?)

Если (данная строка длиннее самой длинной из предыдущих)

ТО

Запомнить ее

Запомнить ее длину

ВСЕ-ЕСЛИ

Напечатать самую длинную строку

ВСЕ-ЦИКЛ

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

# include

# define MAXLINE 1000 // максимальный размер вводимой строки

int getline (char line [], int maxline); // прототип функции getline ()

void copy (char to [], char from[]); // прототип

// печать самой длинной строки

main ()

{ int len; // длина текущей строки

int max; // длина макс из просмотренных строк

char line [MAXLINE]; // текущая строка

char longest [MAXLINE]; // самая длинная строка

max =0;

while ((len=getline (line, MAXLINE)) >0)

if (len > max)

{ max=len;

copy (longest, line);

}

if (max > 0) // была ли хоть одна строка?

printf («%s», longest);

return 0;

}

// getline: читает строку в s, возвращает длину

int getline (char s [], int lim)

{ int c, i;

for (i=0; i

s[i] =c;

if (c= ='n')

{s[i]=c;

++ i;

}

s[i]=''; // конец стринга литер

return i ;

}

// copy: копирует из `from 'в `to'

void copy (char to [], char from [])

{ int i=0;

while ((to [i]=from[i])≠'')

+ + i;

}

Функции main и getline взаимодействуют между собой через пару аргументов и возвращаемое значение. Одни функции возвращают результирующее значение, другие (как copy) нужны только для того, чтобы произвести какие-то действия, не выдавая никакого значения. На месте типа результата в copy стоит void. Это явное указание на то, что никакого значения данная функция не возвращает.

Задание размера массива в определении функции имеет целью резервирование памяти. В самой getline задавать длину массива s нет необходимоcти, так как его размер указан в main.

В конец создаваемого массива литер обязательно помещается литера `' (null — литера, кодируемая нулевым байтом), чтобы пометить конец стринга литер.

Внешние переменные и область действия

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

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

Альтернативой локальным (автоматическим — auto) переменным можно определить внешние (extern) переменные, к которым разрешается обращаться по их именам из любой функции.

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

Внешние переменные сохраняют свои значения и после возврата из функций, их установивших.

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

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

" Определение" переменной (функции) — это место, где переменная (функция) создается и ей отводится память.

" Описание" помещается там, где фиксируется природа переменной (функции), но никакой памяти для нее не отводится.

Основная литература — 1 [36−41], 3 [85−105].

1[23−29], 3[39−53]. 1[14−22], 2[40−67], 3[13−38].

Контрольные вопросы:

1. Какова структура программы на языке Си?

2. Под элементами языка понимаются его базовые конструкции, используемые при написании программ. Каковы элементы языка Си?

3. Операции — это комбинации символов, специфицирующие действия по преобразованию значений. Каковы операции языка Си?

4. Константа — это число, символ или строка символов. Для чего используются в программе константы и каковы типы констант языка Си?

5. Ключевые слова — это предопределенные идентификаторы, которые имеют специальное значение для компилятора. Каков список ключевых слов языка Си?

6. Отдельные исходные файлы можно объединить в один исходный файл, компилируемый как единое целое, посредством директивы препроцессора #include. Какие директивы нужны для работы с текстовой информацией?

7. Каково назначение функции main ()?

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

9. В чем смысл использования именованных констант?

10. Каков приоритет операторов ¦ ¦ и &&, и как вычисляются выражения, связанные этими операторами?

11. Какую роль в Си играют функции?

12. Каким образом обеспечивается способ многопутевого ветвления на языке Си?

13. Каков синтаксис задания типа возвращаемого значения функции?

14. Что происходит, если в процессе вычислений мы выходим на конец функции, отмеченный в тексте последней закрывающей фигурной скобкой?

15. Что такое прототип функции и какую роль он играет с точки зрения интерфейса программы?

16. Что такое передача аргументов «по значению» и «по ссылке» ?

17. Если функция в качестве аргумента получает массив, то каким образом она имеет доступ к любому элементу массива?

18. Каковы основные управляющие конструкции в структурном программировании?

19. Что должна делать main, если встретится строка, превышающая допустимый размер (MAXLINE)?

20. В каких случаях оправдано применение локальных и внешних переменных?

Лекция 6. Операторы языка СИ. Условный оператор. Операторы цикла. Оператор выбора

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

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

Всем переменным разумно давать осмысленные имена в соответствии с их назначением. Короткие имена предпочтительны для локальных переменных, особенно для счетчиков циклов, и более длинные для внешних переменных. Обычно в программах на Си малыми (строчными) буквами набирают переменные, а большими (прописными) — именованные константы.

В Си существует всего лишь несколько базовых типов: char, int, float, double. Имеется также несколько квалификаторов, которые можно использовать вместе с указанными базовыми типами: short, long-применяемые к целым, и signed, unsigned-применяемые к типу char и любому целому типу. Тип long double предназначен для арифметики с плавающей точкой повышенной точности.

Именованные константы для всех размеров вместе с другими характеристиками машины и компилятора содержатся в стандартных головных файлах и .

Целый тип (включая перечислимый тип) и плавающий тип в совокупности образуют арифметический тип.

Тип void (пустой) имеет специальное назначение. Указание спецификации типа void в объявлении функции означает, что функция не возвращает значений.

Указание типа void в списке объявлений аргументов в объявлении функции означает, что функция не принимает аргументов.

Объявление указателя на тип void означает, что он будет ссылаться на любой, т. е. не — специфицированный тип.

Тип void может быть указан в операции приведения типа. Нельзя объявить переменную тип void .

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

Декларации

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

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

К любой переменной в декларации может быть применен квалификатор const для указания того, что ее значение далее не будет изменяться. Применительно к массиву квалификатор const указывает на то, что ни один из его элементов не будет меняться. Указание const можно также применять к аргументу — массиву, чтобы сообщить, что функция не изменяет этот массив.

Арифметические операторы

Бинарными арифметическими операторами являются:

+, —, *, /, %,

где %-подобный оператору mod языка Паскаль — оператор взятия модуля. Оператор % к операндам типов float и doublе не применяется.

Операторами отношения и сравнения являются:

<, <=, >, >=, = = (равно), ≠ (не равно).

Логические операторы: (конъюнкция) и ¦¦ (дизъюнкция).

По определению численным результатом вычисления выражения отношения или логического является 1 в случае, если оно истинно, и 0-если оно ложно.

Оператор ! обычно используют в конструкциях вида

if (!valid) // не правильный

что эквивалентно if (valid = = 0).

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

Если операнды оператора принадлежат разным типам, то они приводятся к некоторому общему типу, типу с большим диапазоном значений. В общем случае, когда оператор имеет разнотипные операнды, прежде чем операция начнет выполняться, «младший» тип подтягивается к «старшему». Результат будет иметь старший тип. К старшим типам, в порядке уменьшения их приоритета, относятся: long double, double, float, int, long.

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

// atoi: преобразование стринга s в число

int atoi (char s[])

{ int i, n=0;

for (i=0; s[i]>='0' s[i]<='9'; i + +)

n =10*n+(s[i]-`0');

return n;

}

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

Так как аргумент в вызове функции есть выражение, при передаче его функции также возможно преобразование типа. При отсутствии прототипа функции аргументы типа char и short переводятся в int, a floatв double. В том случае, когда аргументы описаны в прототипе функции, как и должно быть, при вызове функции нужное преобразование включается автоматически.

Инкрементные и декрементные операторы. Операторы присваивания и выражения

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

Например,

if (c = = `n') + + nline;

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

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

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

В Си имеются шесть операторов для манипулирования с битами. Их можно применять только к операндам типов char, short, int и long, знаковым и беззнаковым.

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

//getbits: получает n бит, начиная с р-ой позиции

unsigned getbits (unsigned x, int p, int n)

{ return (x>>(p+1-n)) & ~(~0<

}

Операторы присваивания и выражения Большинству бинарных операторов т. е. операторов, имеющих левый и правый операнды, соответствуют операторы присваивания op=, где ор — один из операторов + - * / % << >> & ^ ¦.

Например, выражение i=i+2 можно написать в сжатом виде: i+=2.

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

Типом и значением любого выражения присваивания являются тип и значение его левого операнда после завершения присваивания.

Условные выражения Инструкции

if (a>b) z=a;

else z=b;

пересылают в z максимальное из двух значений, а и b.

В Си существует тернарный оператор «?:», который представляет собой другой способ записи этой и подобных ей конструкций:

z=(a>b)? a: b; // z=max (a, b)

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

Условное выражение часто позволяет сократить программу. В качестве примера приведем цикл, обеспечивающий печать n элементов массива по 10 на каждой строке с одним пробелом между колонками; каждая строка цикла, включая последнюю, заканчивается литерой новая — строка:

for (i=0; i

printf (" %6d%c", ar [i],(i%10= = 9¦¦i= = n-1)?'n':' `);

Литера новая-строка посылается после каждого десятого и после n-го элемента. За всеми другими элементами следует пробел.

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

Любое выражение становится инструкцией, если в конце его поставить точку с запятой, как, например, в записи х=0; i++; printf (…);

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

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

Другой пример объединения инструкций — это круглые скобки, помещенное после if, else, while или for.

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

К инструкциям управления относятся:

ifelse — для принятия решения;

elseifдля многоступенчатого принятия решения;

switch — переключатель для выбора одного из многих путей (аналог оператора case в Паскале);

циклы while и for;

цикл dowhile;

инструкции break и continue;

инструкции goto и метки.

Приведем некоторые комментарии к данным инструкциям:

- отсутствие в одной из вложенных друг в друга if — конструкций else — части может привести к не однозначному толкованию записи. Эту неоднозначность разрешают тем, что else всегда связывают с ближайшим if, у которого нет своего else;

— правильной интерпретации вложенности конструкций if и else помогает правильная расстановка фигурных скобок, а также с помощью показа отступов в тексте;

— каждая ветвь переключателя switch (в том числе и после default) должна заканчиваться инструкцией break — выход из переключателя;

— с точки зрения грамматики три компоненты цикла for (выр1; выр2; выр3) представляют собой произвольные выражения, но чаще выр1 и выр3 — это присваивания или вызовы функций, а выр2 — выражение отношения. Любое из этих трех выражений может отсутствовать, но точку с запятой опускать нельзя. При отсутствии выр1 и выр3 считается, что их просто нет в конструкции цикла; при отсутствии выр2 полагается, что его значение как бы всегда истинно;

— в Си индекс цикла for и его предельное значение могут изменяться внутри цикла, и значение индекса цикла после выхода из цикла всегда определено;

— пара выражений, разделенных оператором «запятая», вычисляется слева направо. Типом и значением результата являются тип и значение самого правого выражения, что позволяет в инструкции for в каждой из трех компонент иметь по несколько выражений, например, вести два индекса параллельно (в отличие от запятых, которые разделяют аргументы функции, переменные в описаниях и т. д.);

— повтор в цикле do — while происходит когда условие истинно;

- альтернативой нормальному завершению цикла являются инструкции break и continue: инструкция break вызывает немедленный выход из самого внутреннего из объемлющих ее циклов или переключателей; инструкция continue вынуждает ближайший объемлющий ее цикл (for, while или do — while) начать следующий шаг итерации.

Основная литература — 1[42−70], 2[135−167], 3[120−132], 3[85−106].

Контрольные вопросы:

1. Что такое тип объекта?

2. Что преследует разрешение помечать объекты квалификатором const?

3. Каково количество значимых литер для внутренних имен и для имен функций и внешних переменных?

4. Что такое эскейп — последовательность? Каков полный набор эскейп — последовательностей?

5. Каковы приоритеты операторов языка Си и порядок их выполнения.

6. Пусть int k=2; Чему будет равно значение переменной к после выполнения оператора присваивания: k=k+ + + + + k;

7. Каким образом происходит заполнение освобождающихся разрядов при применении операторов сдвига <<, >>?

8. Какими преимуществами обладают операторы присваивания в сжатом виде?

9. Каким образом определено правило преобразования для тернарного оператора, в случае если входящие в него выражения принадлежат любым типам?

10. Каков приоритет и порядок вычислений всех операторов языка Си?

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

12. Покажите различные способы разрешения неоднозначности при вложенности друг в друга if — конструкций.

Лекция 7. Функции и структура программы

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

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

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

Инструкция return реализует механизм возврата результата от вызываемой к вызывающей функции. За словом return может следовать любое выражение. Если потребуется, выражение будет приведено к типу тип-результата.

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

Если тип результата функции опущен, то предполагается, что она возвращает значение типа int.

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

#include

#define MAXLINE 100

// примитивный калькулятор

main ()

{ double sum, atof (char []);

char line [MAXLINE];

int getline (char line [], int max);

sum=0;

while (getline (line, MAXLINE) >0)

printf (" t%gn", sum+=atof (line));

return 0;

}

В декларации double sum, atof (char []); говорится, что sum — переменная типа double, а atofфункция, которая принимает аргумент типа char [] и возвращает результат типа double (преобразует стринг в double).

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

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

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

Для любой функции внешняя (external) переменная доступна по ее имени, если это имя было должным образом описано в этой функции.

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

Функции: правила областей действия. Статические объекты

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

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

область действия внешней переменной или функции простирается от точки программы, где она декларирована до конца файла, подлежащего компиляции. Например, если main, sp, val, push и pop определены в одном файле в указанном порядке, т. е.

main () { … }

int sp=0;

double val [MAX];

void push (double f) { … }

double pop (void) { … }

то к переменным sp и val можно адресоваться из push и pop просто по их именам; никаких дополнительных деклараций для них не требуется. В main эти имена не видимы, как и сами push и pop.

Если на внешнюю переменную нужно сослаться до того, как она определена, или если она определена в другом файле, то ее декларация должна быть помечена словом extern. Декларация (описание) внешней переменной объявляет свойства переменной (прежде всего ее тип), а определение, кроме того, приводит к определению для нее памяти.

Если строки

int sp;

double val [MAX];

расположены вне всех функций, то они определяют внешние переменные sp и val, т. е. отводят для них память, и, кроме того служат декларациями для остальной части исходного файла. А вот строки

extern int sp;

extern double val [ ];

декларируют для остальной части файла, что sp — переменная типа int и что val — массив типа double (размер которого определен где-то в другом месте); при этом ни переменная ни массив не создаются, и память им не отводится.

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

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

Инициализировать внешнюю переменную можно только в определении!

Пусть определим функции push и pop в одном файле, а переменные sp и val — в другом, где их и инициализируем. Тогда для установления связей понадобятся такие определения и декларации:

В файле 1:

extern int sp;

extern double val [ ];

void push (double f) { … }

double pop (void) { … }

В файле 2:

int sp=0;

double val [MAX];

Поскольку декларации extern находятся в начале файла 1 и вне определений функций, их действие распространяется на все функции, причем одного набора деклараций достаточно для всего файла 1. Та же организация extern — деклараций необходима и в случае, когда программа состоит из одного файла, но определения sp и val расположены после их использования.

Указание static, примененное к внешней переменной или функции, ограничивает область действия объекта концом файла. Это есть способ скрыть имена от других файлов. Статическая память специфицируется словом static, которое помещается перед обычным описанием. Указание static перед декларациями переменных sp и val, с которыми работают только push и pop, скрывает их от остальных функций.

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

Если же функция помечена словом static, то ее имя становится невидимым вне файла, в котором она определена.

Декларацию static можно использовать и для внутренних переменных.

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

Основная литература — 1[71−93], 2[256−268], 3[133−145].

Контрольные вопросы:

1. Что такое функция?

2. Что специфицирует определение функции?

3. Что задает объявление функции?

4. В определении функции допускается указание спецификации класса памяти static или extern. Что такая спецификация задает?

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

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

Пусть имеет следующие декларации:

int x=1, y=2, z[10];

int *ip; //ip — указатель на int

Декларация int*ip (символ * «прижимается» к идентификатору переменной) гласит: «выражение *ip есть нечто типа int» .

Унарный оператор &(амперсанд) выдает адрес объекта. Унарный оператор * есть оператор раскрытия ссылки. Этот оператор применяется только к указателю и выдает содержимое объекта, на который указатель ссылается.

Следующие несколько строк показывают каким образом используются операторы * и &.

ip=&x; //указателю присвоен адрес переменной x, ip указывает на x

y=*ip; //y теперь равен 1, а не 2

*ip=0; //х теперь равен 0

ip=z; //ip указывает на 0-й элемент (начало) массива z

ip = &z [5]; //ip теперь указывает на 5-й элемент массива z

Таким образом, синтаксис декларации переменной указатель «подстраивается» под синтаксис выражений, в которых эта переменная может встретиться. Указанный принцип применим и в отношении описаний функций. Например, запись double *dp, atof (char*);означает, что выражение *dp и atof (s) имеют тип double, а аргумент функции atof есть указатель на char (фактически, указатель на стринг).

Указателю разрешено ссылаться только на объекты заданного типа.

Указатель на void может ссылаться на объекты любого типа, но к такому указателю нельзя применять оператор раскрытия ссылки.

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

Указатели и аргументы функций. Указатели и массивы.

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

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

Рассмотрим функцию swap, которая в программе сортировки переставляет местами два неупорядоченных элемента.

Если функция swap определена следующим образом:

void swap (int x, int y)

{ int temp;

temp=x; x=y; y=temp;

}

тогда вызов swap (a, b) никак не повлияет на переменные a и b, поскольку swap получает лишь копии их значений. Для достижения эффекта — перестановки, формальные параметры функции swap должны быть описаны как указатели, а аргументы в вызове функции представлены адресами соответствующих параметров — swap (&a, &b).

void swap (int *px, int *py)

{int temp;

temp=*px; *px=*py; *py=temp;

}

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

Декларация int ar[10]; определяет массив ar размера 10, т. е. блок из 10 последовательных объектов с именами ar[0], ar[1],…, ar[9]. Запись ar[i] означает i-й элемент массива.

Если pa есть указатель на int, т. е. определен как int * pa; то в результате присваивания pa=&a[0]; указатель pa будет указывать (ссылаться) на нулевой элемент ar; иначе говоря, pa будет содержать адрес элемента ar[0]. Присваивание x=* pa; будет копировать содержимое ar[0] в x.

Если pa указывает на некоторый элемент массива, то pa+1 по определению указывает на следующий элемент, pa+i — на i-й элемент после pa, а pa-i — на i — й элемент перед pa. Таким образом, если pa указывает на ar[0], то *(pa+1) есть содержимое ar[1], pa+i — адрес ar[i], а *(pa+i) — содержимое ar[i].

Таким образом, элемент массива одинаково разрешается изображать и в виде указателя со смещением и в виде массива с индексом.

Указатель — это переменная, а имя массива это константный указатель, по этой причине можно написать pa=а или ра+ +, а записи типа ar=pa или ar+ + не допускаются.

Если имя массива передается функции. то последняя получает в качестве аргумента адрес его начального элемента. Внутри вызываемой функции этот аргумент является локальной переменной, содержащей адрес. Вообще, функция может рассматривать имя массива в качестве своего аргумента так, как ей удобно — либо как имя массива (char s [ ]), либо как указатель (char *s), и поступать с ним соответственно. Функция может даже использовать оба вида записи, если это покажется ей уместным.

Адресная арифметика. Литерные указатели и функции.

Если р есть указатель на некоторый элемент массива, то р+ + продвигает р так, чтобы он указывал на следующий элемент, а р+=i увеличивает его, чтобы он указывал на i-й элемент после того, на который он указывал ранее. Эти и подобные конструкции — самые простые примеры арифметики над указателями, называемой так же адресной арифметикой.

Соединение в одном языке Си указателей, массивов и адресной арифметики — одна из сильных его сторон.

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

Декларация static char *p=ar; определяет р как указатель на char и инициализирует его адресом массива ar. Указанная декларация могла бы иметь и такой вид:

static char *p=&ar[0]

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

Отметим несколько важных свойств арифметики с указателями:

— при соблюдении некоторых правил указатели можно сравнить. Если p и q указывают на элементы одного массива, то к ним можно применять операторы отношения = =, ≠, <, <=, > и т. д. Например отношение вида p

— любой указатель всегда можно сравнить на равенство и неравенство с нулем;

— для указателей, ссылающихся на элементы разных массивов, результат арифметических операций или сравнений не определен;

— указатели и целые можно складывать и вычитывать. Запись p+n означает адрес объекта, занимающего n-ое место после объекта на который указывает р. Это справедливо безотносительно к типу объекта, на который ссылается р; n автоматически домножается на коэффициент, соответствующий размеру (типу) объекта. Информация о размере неявно присутствует в описании р. Если, к примеру, int занимает четыре байта, то коэффициент умножения будет равен четырем;

— допускается также вычитание указателей. Например, если p и q ссылаются на элементы одного массива и р

— арифметика с указателями учитывает тип объекта: если она имеет дело со значениями float, занимающими больше памяти, чем char, и р-указатель на float, то р+ + продвинет р на следующее значение float;

Стринговая константа, написанная в виде «I am a string» есть массив литер. Во внутреннем представлении этот массив заканчивается «пустой» литерой `', по которой программа может найти конец стринга.

Доступ к стринговой константе осуществляется через указатель на ее первый элемент.

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

char *pmessage;

Присваивание pmessage="now is the time"; поместим в эту переменную ссылку на литерный массив, при этом сам стринг не копируется, а копируется лишь указатель на него.

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

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

char armessage [ ] = «now is the time» ;

char *pmessage = «now is the time» ;

соответственно.

Между этими двумя определениями существует важное различие в силу того, что указатель это переменная, а имя массива — константный указатель.

Основная литература — 1[94−106], 3[65−66]., 2[269−280].

Контрольные вопросы

1.Каков приоритет унарных операторов * и & и порядок их выполнения в выражении?

2. Пусть имеем следующие декларации

int k=2;

int *ip;

Как можно изменить содержимое переменной k на 5 с помощью указателя ip?

3. Есть ли разница в записях (*ip)+ + и *ip+ +?

4. Если *ip+=1увеличивает на единицу то, на что ссылается ip, то какие действия выполняют операторы:

+ +*ip и (*ip)+ +?

5. Если имеем два указателя на int: ip и iq, то к чему приведет оператор присваивания

iq=ip;

6. Что значит передать данные в функцию «по значению» ?

7. Изобразите графически механизм доступа к исходным данным, если параметры функции описаны как указатели.

Лекция 9. Использование сложных типов в языке СИ. Описание массивов. Программирование ввода-вывода одномерных массивов. Строки. Обработка строк.

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

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

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

Сами же указатели можно организовывать в виде массива. Одна из возможностей сравнить две строки — передать указатели на них функции strcmp. Чтобы поменять местами строки, достаточно будет поменять местами в массиве их указатели (а не сами строки) .

При этом снимаются сразу две проблемы: однасвязанная со сложностью управления памятью, а вторая — с большими накладными расходами при перестановках самих строк.

Процесс сортировки распадается на три этапа:

- чтение всех строк из ввода;

— сортировка введенных строк;

— печать их по порядку.

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

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

В качестве алгоритма сортировки можно предложить быструю сортировку, предложенную К. А. Хоором в 1962 году.

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

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

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

Представим декларацию двумерного массива

int ar[5] [10];

Если массив ar передается некоторой функции f, то эту функцию можно было бы определить следующим образом:

f (int array[5][10]) {…};

Вместо этого можно записать:

f (int array [][10]) {…}

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

f (int (*array)[10]) {…}

последняя запись декларирует, что параметр есть указатель на массив из 10 значений типа int. Скобки здесь необходимы, так как квадратные скобки [] имеют более высокий приоритет, чем *. Без скобок (круглых) декларация

int * array[10]

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

Основная литература — 1[107−111], 2[346−370].

Контрольные вопросы:

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

2. Какие проблемы решаются при сортировке текстовых строк с использование указателей?

3. О чем говорит следующая декларация char * lineptr[MAXLINES]?

4. Используя декларацию п. 3, что собою представляет следующее выражение *lineptr [i]?

5. Почему при передачи функции двумерного массива количество строк массива может не указываться в параметре функции?

Лекция 10. Инициализация массивов указателей. Указатели вместо многомерных массивов Покажем механизм инициализации массивов указателей на примере на функции month_name (n), которая возвращает ссылку на стринг литер, содержащий название n-го месяца. Эта функция идеальна для демонстрации использования статического массива. Функция имеет в своем распоряжении массив стрингов, на один из которых она и возвращает ссылку.

month_name: возвращаем имя n-го месяца

char * month _ name (int n)

{ static char * name [] {" Неверный месяц" ,

" Январь", «Февраль», «Март», «Апрель», «Май», «Июнь», «Июль», «Август», «Сентябрь», «Октябрь», «Ноябрь», «Декабрь» };

return (n<1¦¦ n>12)? name [0]: name [n];

Инициализатором служит список стрингов каждому из которых соответствует определенное место в массиве. Литеры і-го стринга где-то размещены в памяти и указатель на них запоминается в name [і]. Так как размер массива name не специфицирован, компилятор вычислит его по количеству заданных начальных значений (инициализирующих выражений).

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

int ar [10][20];

int*x[10];

Записи ar[3] и x[3] будет синтаксически правильными ссылками на некоторое значение типа int. Однако только ar является истинно двумерным массивом: для двух сом элементов типа int будет выделена память, а вычисление смещения элемента ar [строка, столбец] от начало массива будет вестись по формуле 20 * строка + столбец, учитывающей его прямоугольную природу.

Для массива x определяются только 10 указателей, причем без инициализации. Инициализация должна задаваться явно — либо статически, либо в процессе счета.

Если каждый элемент x ссылается на пятиэлементный массив, то в результате в памяти будет выделено пространство для размещения 50 значений типа int, и еще 10 ячеек для указателей.

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

В операционной среде, обеспечивающей поддержку Си, имеется возможность передать аргументы или параметры запускаемой программе при помощи командной строки. В момент вызова main получает два аргумента. В первом, обычно называемом argc (argument count), стоит количество аргументов, задаваемых в командной строке. Второй, argv (argument vector), являются указателем на массив литерных стрингов, содержащих сами аргументы. Для работы с этими стрингами обычно используются указатели нескольких уровней.

Простейший пример-программа с именем echo (эхо), которая печатает аргументы своей командной строки в одной строке, отделяя их друг от друга пробелами. Так команда

Echo Здравствуй, мир!

Напечатает Здравствуй, мир!

# include

main (int argc, char*argv [])

{int i;

for (i=1; i

printf («% s % s», argv [i], (i

printf («n»);

return 0 ;

}

По соглашению argv есть имя вызываемой программы, так что значение argc никогда не бывает меньше 1. Если argc=1, то в командной строке после имени программы никаких аргументов нет. В нашем примере argc равен 3, и соответственно argv [0]= «echo»; argv [1]= «Здравствуй ,», argv [2]= «мир!». Стандарт требует, чтобы argv [argc] всегда был пустым указателем.

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

# include

main (int argc, char * argv [])

{while (—argc >0)

printf («% s % s» ,*++ argv,(argc>1)?'':'');

printf («n»);

return 0 ;

}

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

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

Для иллюстрации этих возможностей воспользуемся программой сортировки, которая при задании необязательного аргумента — n упорядочивала строки по их числовому значению, а не в лексикографическом порядке.

Сортировка, как правило, распадается на три части:

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

— перестановку, меняющую порядок пары объектов на обратный;

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

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

#include

#include

#define MAXLINES 500 \ максим. Число строк

char *lineptr [MAXLINES]; \ массив указателей на строки

int readlines (char *lineptr[ ], int nlines);

void writelines (char *lineptr[ ], int nlines);

void qsort (void* lineptr[ ], int left, int right, int (*comp)(void*, void*));

int numcmp (char*, char*);

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

main (int argc, char*argv[ ])

{int nlines;\ кол-во прочитанных строк

int numeric=0; \ 1, если сортировка по числовому значению

if (argc>1 && strcmp (argv[1]," -n")= =0)

numeric=1;

if ((nlines=readlines (lineptr, MAXLINES))>=0)

{qsort ((void**) lineptr, 0, nlines-1,

(int (*)(void*, void*))(numeric?numcmp:strcmp));

writelines (lineptr, nlines);

return 0;

}

else

{printf («введено слишком много строк n»);

return 1;

}

}

В обращениях к функциям qsort, strcmp и numcmp их имена трактуются как адреса этих функций. Поэтому оператор & перед ними не нужен, как он не был нужен и перед именем массива (имя массива и имя функции — это указатели).

Функция qsort может обрабатывать данные любого типа, а не только стринги. Как видно из прототипа, функция qsort в качестве своих аргументов ожидает: массив ссылок, два целых значения и функцию с двумя аргументами — указателями. В качестве указателей — аргументов заданы указатели обобщенного типа void*. Любой указатель можно привести к типу void* и обратно без потери информации. Поэтому мы можем обратиться к qsort, предварительно преобразовав аргументы в void*. Внутри функции сравнения ее аргументы будут приведены к нужному ей типу. На самом деле эти преобразования никакого влияния на представления аргументов не оказывают, они лишь обеспечивают согласованность типов для компилятора.

\ qsort — сортирует v[left]…v[right] по возрастанию

void qsort (void*v[ ], int left, int right, int (*comp)(void*, void *))

{int i, last;

void (swap (void *v[ ], int, int);

if (left>=right) //ничего не делается, если в массиве менее двух элементов

return;

swap (v, left,(left+right)/2);

last=left;

for (i=left+1; i<=right; i++)

if ((*comp)(v[i], v[left])<0)

swap (v, ++last, i);

swap (v, left, last);

qsort (v, left, last-1, comp);

qsort (v, last+1, right, comp);

}

Обратите внимание на четвертый параметр функции qsort

int (*comp)(void*, void*)

Этот параметр сообщает, что comp есть указатель на функцию, которая имеет два аргумента — указателя и выдает результат типа int. Таким образом, так как comp есть указатель на функцию, *comp — есть функция, то выражение (*comp) (v[i], v[left]) — обращение (вызов) к ней!

Приведем текст функции numcmp, которая сравнивает два стринга, рассматривая их как числа; предварительно они переводятся в числовые значения функцией atof.

#include

\ numcmp: сравнивает s1 и s2 как числа

int numcmp (char *s1, char *s2)

{double v1, v2;

v1=atof (s1); v2=atof (s2);

if (v1

else if (v1>v2) return 1;

else return 0;

}

Сложные декларации. Преобразование указателей.

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

Проблему иллюстрирует различие следующих двух деклараций:

int * f (); // f возвращает указатель на int

int (* pf)(); // pf — указатель на функцию, которая возвращает int

В силу того, что приоритет префиксного оператора * ниже, чем приоритет оператора (), поэтому во втором случае скобки обязательны.

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

char ** argv, argv — указатель на указатель на char;

int (*array), array — указатель на массив из int;

void* comp (), comp — функция возвращающая укзатель на void;

void (* comp) (), comp — указатель на функцию возвращающую void;

char (*(*f ())[](), f — функция возвращающая указатель на массив [] из указателей на функцию возвращающую char;

char (*(*ar[3])()), ar — массив из указателей на функцию возвращающую указатель на массив из char;

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

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

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

Указатели на любые типы могут быть преобразованы к указателям на функции, и обратно.

Специальные ключевые слова near, far, huge позволяют модифицировать формат и размер указателей в программе. Компилятор учитывает принятый в выбранной модели памяти размер указателей и может неявно производить соответствующие преобразования адресных значений. Так, передача указателя в качестве аргумента функции может вызвать неявное преобразование его размера к большому из следующих двух значений:

— принятому по умолчанию размеру указателя для действующей модели памяти;

— размеру типа аргумента.

Если задан прототип функции (предварительное объявление функции), в котором указан явно тип аргумента — указателя, в т. ч. с модификатором near, far, huge, то будет преобразование именно к этому типу.

Если объявлен указатель на функцию, то в приведении его типа можно задавать другие типы аргументов. Например,

int (*p) (long); // объявление указателя на функцию, которая ожидает

// значение типа long, и возвращает int

(*(int (*)(int))p)(0); — вызов функции по указателю.

Основная литература — 1[112−124], 2[346−370].

Контрольные вопросы:

1. В чем суть инициализации массива указателей?

2. В чем разница между двумерным массивом и массивом указателей?

3. Каким образом происходит вычисление смещения элемента двумерного массива от его начала?

4. Так как массивы указателей чаще используются для работы со стрингами литер, различающимися по длине, дайте сравнительную оценку определения массива указателей сhar*name [] = {" Неправильный месяц", «январь», «февраль», «март» };

С определением двумерного массива:

char*aname [] [15]= {" Неправильный месяц", «январь», «февраль», «март» };

5. Для определений п. 4 сделайте соответствующие рисунки.

6. Какие операции допустимы над указателями на функции?

7. Каким образом трактуются имена функций?

8. В декларации (*comp) (void*, void*) скобки нужны, что обеспечивает правильную трактовку указателя на функцию. Что будет описывать декларация без скобок: *comp (void*, void*)?

9. Разберите алгоритм функции atof — перевода стринга в числовое значение.

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

11. В чем суть операторов () и []? Каков их приоритет в выражении?

12. Что представляет собой декларация

13. (*pfa[])()?

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

15. Могут ли указатели на любые типы данных преобразованы к указателям на функции, и наоборот?

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

Лекция 11. Структуры данных. Описание структур. Указатели и структуры данных.

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

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

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

Декларация структуры «точка» будет выглядеть следующим образом

struct point

{int x;\ в фигурных скобках список деклараций

int y;

};

point — это тег (имя) структуры (tag — ярлык, этикетка).

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

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

Декларация структуры — это тип. Запись

struct {…}x, y, z;

означает описание трех переменных структурного типа.

Декларация структуры, не содержащей списка переменных, не резервирует памяти: она просто описывает шаблон, или образец структуры. Если структура имеет тег, то этим тегом далее можно пользоваться при определении структурных объектов. Например, декларация

struct point pt;

определяет структурную переменную pt типа struct point.

Структурную переменную при ее определении можно инициализировать:

struct point maxpt = {320, 200};

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

Доступ к отдельному члену структуры осуществляется посредством конструкции вида:

имя_структуры.член

Например, печать координат точки pt можно осуществить следующим образом:

printf («%d,%d», pt. x, pt. y);

Структуры могут быть вложены друг в друга.

Структуры и функции. Массивы структур.

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

Существует три подхода для передачи функциям структурных объектов:

— передавать компоненты (члены) по отдельности;

— передавать всю структуру целиком;

— передавать указатель на структуру.

Если функции передается большая структура, то, чем копировать ее целиком, эффективнее передать указатель на нее.

Декларация

struct point *pp;

сообщает, что рр есть указатель на структуру типа struct point. Если рр ссылается (имеет адрес) на структуру point, то *рр есть сама структура, а (*рр).х и (*рр).у — члены структуры point. Используя указатель рр, можно написать

struct point origin, *pp;

pp=&origin;

printf («origin:(%d,%d)n» ,(*pp).x,(*pp).y);

Скобки в (*рр).х необходимы, поскольку приоритет оператора. выше чем приоритет оператора *.

В связи с понятием «указатель на структуру» была введена еще одна, более короткая форма записи для доступа к ее членам. Если руказатель на структуру, то

p->член_структуры

есть ее отдельный член. Поэтому printf можно переписать в виде

printf («origin:(%d,%d)n», pp->x, pp->y);

Оба оператора. и -> выполняются слева направо.

Операторы доступа к членам структуры. и -> вместе с операторами вызова функции () и индексации массива [ ] занимают самое высокое положение в иерархии приоритетов и выполняются раньше любых других операторов.

Например, если задана декларация

struct

{int len;

char*str;

}*p;

то ++р -> len увеличит на 1 значение члена структуры len, а не сам указатель, поскольку в этом выражении как бы неявно присутствуют скобки:

++(р->len).

Для изменения порядка выполнения операций нужны явные скобки. Так, в (++р)->len, прежде чем взять значение len, программа продвинет указатель р.

Массивы структур

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

char *keyword [NKEYS];

int keycount [NKEYS];

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

char *word;

int count;

Такие пары составляют массив. Декларация

struct key

{char *word;

int count;

}keytab[NKEYS];

описывает структуру типа key и определяет массив keytab, каждый элемент которого есть структура этого типа и которому где-то будет выделена память. Это же можно записать и по-другому:

struct key

{char *word;

int count;

};

struct key keytab[NKEYS];

Так как массив keytab содержит постоянный набор имен, его легче всего сделать внешним массивом и инициализировать один раз в момент определения:

struct key{

char *word;

int count;

} keytab[ ]={" auto", 0," break", 0,/*…*/" while", 0};

Число элементов массива keytab будет вычислено по количеству инициализаторов.

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

Sizeof объект и sizeof (имя типа)

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

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

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

#include

#include

#include

#define MAXWORD 100

int getword (char*, int);

struct key * binsearch (char*, struct key *, int);

\ подсчет ключевых слов Си

main ()

{char word [MAXWORD];

struct key *p;

while (getword (word, MAXWORD)≠EOF)

if (isalpha (word[0]))

if ((p=binsearch (word, keytab, NKEYS))≠NULL)

p->count++;

for (p=keytab; p

if (p->count > 0)

printf («%4d%sn», p->count, p->word);

return 0;

}

// binsearch: найти слово в tab[0]…tab[n-1]

struct key * binsearch (char *word, struct key *tab, int n)

{int cond;

struct key *low=&tab[0];

struct key * high = &tab[n];

struct key *mid;

while (low

{mid=low+(high — low)/2;

if ((cond=strcmp (word, mid->word))<0)

high=mid;

else if (cond > 0)

low=mid+1;

else

return mid;

}

return NULL;

}

Если функция binsearch находит ключевое слово, то она выдает указатель на него, в противном случае она возвращает NULL. К элементам массива keytab доступ осуществляется через указатель. Инициализаторами для low и high служат указатели на начало и на место сразу после конца массива. Вычисление положения среднего элемента с помощью формулы

mid = (low +high)/2

не годится, поскольку указатели нельзя складывать. Однако к ним можно применить операцию вычитания, и так как high-low есть число элементов, присваивание

mid = low + (high — low)/2

установит в mid указатель на элемент, лежащий посередине между low и high.

В цикле for оператор p++ увеличит р на такую величину, чтобы выйти на следующий структурный элемент массива. Не следует полагать, что размер структуры равен сумме размеров ее членов. Оператор sizeof возвращает правильное значение. Функция getword принимает следующее слово или литеру из входного потока. Под словом понимается цепочка букв — цифр, начинающаяся с буквы, или отдельная непробельная литера. По концу файла функция выдает EOF, в остальных случаях ее значением является код первой литеры слова или код отдельной литеры, если она не буква.

\ getword: принимает следующее слово или литеру

int getword (char * word, int lim)

{int c, getch (void);

void ungetch (int);

char * w =word;

while (isspace (c=getch ())

;

if (c≠EOF)*w++=c;

if (!isalpha (c))

{*w='';

return c;

}

for (; —lim >0; w++)

if (!isalnum (*w=getch ()))

{ungetch (*w);

break;

}

*w='';

return word [0];

}

Обращение к ungetch позволяет вернуть лишнюю литеру во входной поток. В getword используются также isspace — для пропуска пробельных литер, isalpha — для идентификации букв и isalnum — для распознавания букв — цифр. Все они описаны в стандартном головном файле. Как работают функции getch и ungetch?

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

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

Структуры со ссылками на себя

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

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

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

- указатель на текст слова

— счетчик числа встречаемости

— указатель на левый сыновний узел

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

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

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

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

struct tnode //узел дерева

{char *word;//указатель на текст

int count;//число вхождений

struct tnode*left;//левый сын

struct tnode*right;// правый сын

};

Структура не может включать саму себя, но ведь struct tnode*left; определяет left как указатель на узел tnode, а не сам tnode.

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

Прием, позволяющий справляться с этой задачей, демонстрирует следующий фрагмент:

struct t

{

struct s *p; // p указывает на s

};

struct s

{

struct t *q; // q указывает на t

};

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

#include

#include

#include

#define MAXWORD 100

struct tnode * addtree (struct tnode *, char *);

void treeprint (struct tnode *);

int getword (char *, int);

// подсчет частоты встречаемости слов

main ()

{ struct tnode * root; // указатель на корневой узел

char word [MAXWORD];

root = NULL;

while (getword (word, MAXWORD)≠EOF)

if (isalpha (word[0]))

root = addtree (root, word);

treeprint (root);

return 0;

}

Функция addtree рекурсивна. Для каждого вновь поступившего слова ищется его копия и в счетчик добавляется 1, либо для него заводится новый узел, если такое слово в дереве отсутствует. Создание нового узла сопровождается тем, что addtree возвращает на него указатель, который вставляется в узел родителя.

struct tnode * talloc (void);

char * strdup (char *);

// addtree: добавляет узел со словом w в p или ниже него

struct tnode * addtree (struct tnode *p, char * w)

{int cond;

if (p= = NULL)//слово встречается впервые

{ p=talloc (); // создается новый узел

p->word = strdup (w);

p->count =1;

p->left = p-> right = NULL;

}

else if ((cond = strcmp (w, p->word))= = 0)

p->count + +; // это слово уже встречалось

else if (cond<0) // корня < левого поддерева

p->eft=addtree (p->left, w);

else

p->right=addtree (p->right, w);

return p;

}

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

# include

// talloc: создает tnode

struct tnode * talloc (void)

{return (struct tnode *) malloc (sizeof (struct tnode));

}

// strdup: копирует стринг, указанный в аргументе,

//в место, полученное с помощью malloc

char * strdup (char*s)

{char *p;

p=(char*) malloc (strlen (s) + 1); // +1для `'

if (p≠NULL) // если память выделена

strcpy (p, s);

return p;

}

Функция treeprint печатает дерево в лексикографическом порядке; для каждого узла она печатает его левое поддерево (все слова, которые меньше слова данного узла), затем само слово и, наконец, правое поддерево (все слова, которые больше слова данного узла).

// treeprint: упорядоченная печать дерева p

void treeprint (struct tnode * p)

{if (p≠NULL) //в дереве имеется хотя бы один узел

{treeprint (p->left);

printf (" %4d%sn", p->count, p->word);

treeprint (p->right);

}

}

Просмотр таблиц

Для иллюстрации новых аспектов применения структур, приведем ядро пакета программ, осуществляющих вставку элементов в таблицы и их поиск внутри таблиц. Этот пакет — типичный набор программ, с помощью которых работают с таблицами имен в любом макропроцессоре или компиляторе. Рассмотрим, например, инструкцию #define. Когда встречается строка вида

#define IN 1

имя IN и замещающий его текст 1 должны запоминаться в таблице. Если затем это имя IN встретится в инструкции, например, в state=IN; оно должно быть заменено на единицу.

Существуют две программы, манипулирующие с именами и замещающими их текстами. Это install (s, t), которая записывает имя s и замещающий его текст t в таблицу (s и t — стринги), и lookup (s), осуществляющая поиск s в таблице и возвращающая указатель на место, где имя s было найдено, либо NULL — если s в таблице не оказалось.

Алгоритм основан на «хэшировании» (функции расстановки): поступающее имя свертывается в неотрицательное число (хэш-код), которое затем используется в качестве индекса в массиве указателей. Каждый элемент этого массива является указателем на начало связанного ссылками списка блоков, описывающих имена с данным хэш-кодом. Если элемент массива содержит NULL, это значит, что среди имен не встретилось ни одного с соответствующим хэш-кодом.

Блок в списке — это структура, содержащая указатели на имя, на замещающий текст и на следующий блок в списке; значение NULL в указателе на следующий блок означает конец списка.

struct nlist\ элемент таблицы

{struct nlist * next;\ указатель на следующий блок списка

char * name;\ определяемое имя

char * defn;\ замещающий текст

};

А вот как записывается определение массива указателей:

#define HASHSIZE 101

static struct nlist *hashtab[HASHSIZE]; \ таблица указателей

\ hash: получает хэш-код по стрингу s

unsigned hash (char * s)

{unsigned hashval;

for (hashval=0; *s≠''; s++)

hashval = *s+31*hashval;

return hashval %HASHSIZE;

}

\ lookup: поиск элемента с s

struct nlist * lookup (char * s)

{struct nlist *np;

for (np = hashtab [hash (s)]; np≠NULL; np=np->next)

if (strcmp (s, np->name)= =0)

return np;\ нашли вставляемый стринг

return NULL;\ не нашли

}

Функция install обращается к lookup, чтобы определить, имеется ли в наличии вставляемый стринг. Если это так, то старое определение будет заменено новым. В противном случае будет образован новый элемент.

struct nlist * lookup (char *);

char * strdup (char *);

\ install: заносит (name, defn) в таблицу

struct nlist * install (char * name, char * defn)

{struct nlist * np;

unsigned hashval;

if ((np=(lookup (name))= =NULL) \ не найден

(np -> name =strdup (name))= =NULL)

return NULL;

hashval = hash (name);

np -> next = hashtab [hashval];

hashtab [hashval] = np;

else\ уже имеется

free ((void *) np-> defn);\ освобождаем прежний defn

if ((np -> defn = strdup (defn)) = =NULL)

return NULL;

return np;

}

Основная литература — 1[125−142], 2[406−436].

Контрольные вопросы:

1. Дайте определение структуры и приведите аналог этого типа в языке Паскаль.

2. Что будет являться множеством значений данного типа?

3. Какие операции допустимы над структурами?

4. Почему допускается совпадение имен членов разных структур, а также совпадение имен тегов, членов и структурных объектов в одной декларации?

5. Учитывая возможность вложения структур приведите декларацию прямоугольника с использованием структуры point.

6. Перечислите единственно возможные операции над структурами.

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

8. Каким образом передаются структурные параметры в функцию?

9. Объясните результат выполнения операций

*p->strи*p->str++,

где str — указатель на стринг из декларации структуры, приведенной в лекции.

Что произойдет после следующих операций:

(*p -> str) + +, *p + + -> str.

10. Что возвращает результатом функция binsearch?

11. Каков механизм передачи параметров в функцию binsearch?

12. Почему вычисление положения среднего элемента с помощью формулы

mid = (low + high)/2 не верно, и каким образом правильно вычислить?

13. Если структура содержит разнотипные члены, то размер структуры будет равен сумме размеров ее членов?

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

Лекция 12. Доступ к файлам

Во всех предыдущих примерах мы имели дело со стандартным вводом и стандартным выводом, которые для программы автоматически предопределены операционной системой конкретной машины.

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

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

Этот указатель, называемый указателем файла, ссылается на структуру, содержащую информацию о файле (адрес буфера, положение текущей литеры в буфере, открыт файл для чтения или на запись, были ли ошибки при работе с файлом и встретился ли конец файла). Пользователю не надо знать подробности, поскольку определения, полученные из, включают описание такой структуры, называемой FILE. Единственное, что требуется для определения указателя файла, — это задать декларацию вида:

FILE * fp;

FILE * fopen (char*name, char* mode);

Из этой записи следует, что fp есть указатель на FILE, а fopen возвращает указатель на FILE. Необходимо заметить, что FILE есть имя типа, а не тег структуры.

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

fp= fopen (name, mode);

Первый аргумент — стринг, содержит имя файла, второй — несет информацию о режиме.

Это тоже стринг: в нем указывается, каким образом пользователь намерен использовать файл.

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

Существует несколько способов чтения из файлов и записи в файл. Самый простой это воспользоваться функциями getc и putc. Функция

int getc (FILE*fp)

возвращает следующую литеру из потока, на который ссылаются при помощи *fp; в случае исчерпания файла или ошибки функция возвращает EOF.

Функция

int putc (intc, FILE*fp)

записывает литеру с в файл fp и возвращает записанную литеру или EOF, в случае ошибки.

При запуске Си — программы операционная системы всегда открывает три файла и обеспечивает три файловые ссылки на них. Этими файлами являются: стандартный ввод, стандартный вывод, и стандартный файл ошибок; соответствующие им указатели называются stdin, stdout и stderr; они описаны в < stdio. h>.

Форматный ввод-вывод файлов можно построить на функциях fscanf и fprintf. Они идентичны scanf и printf с той лишь разницей, что первым их аргументом является указатель на файл.

int fscanf (FILE*fp, char * format,…)

int fprintf (FILE*fp, char * format,…)

В качестве примера, рассмотрим программу конкатенации файлов. Если в командной строке присутствуют аргументы, они рассматриваются как имена файлов для конкатенации, если же аргументов нет, то обработке подвергается стандартный ввод.

# include

main (int argc, char*argv[])

{FILE *fp;

void filecopy (FILE*, FILE*);

if (argc= = 1) // нет аргументов, копируется стандартный ввод

filecopy (stdin, stdout);

else

while (- -argc >0)

if ((fp = fopen (* + + argv, «r»))= = NULL)

{ printf («cat: не могу открыть файл % sn» ,*argv);

return 1;

}

else

{filecopy (fp, stdout);

fclose (fp);

}

return 0;

}

//filecopy: копирует файл ifp в файл ofp

void filecopy (FILE*ifp, FILE*ofp)

{int c;

while ((c=getc (ifp))≠EOF)

putc (c, ofp);

}

Файловые указатели stdio и stdout представляют собой объекты типа FILE*. Это константы, а не переменные, следовательно им нельзя ничего присваивать.

Функция int fclose (FILE*fp) — обратная по отношению к fopen; она разрывает связь между файловым указателем и внешним именем.

Основная литература — 1[155−159], 2[437−458]

Контрольные вопросы:

1. Какую информацию содержит структура, на которую ссылается указатель файла?

2. Что собой представляет FILE?

3. Перечислите возможные режимы и их обозначения использования файла.

4. Какие три файла открывает операционная система при запуске Сипрограммы? Их назначения.

5. Укажите причины применения fclose к открытым файлам, в том числе и к файлу вывода.

Управление ошибками. Ввод-вывод строк.

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

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

Перепишем программу конкатенации так, чтобы сообщения об ошибках отправлялись в stderr.

# include < stdio. h>

// конкатенация файлов

main (int argc, char * argv[])

{FILE * fp;

void filecopy (FILE*, FILE*);

char * prog = argv[0]; // имя программы

if (argc= =1) // нет аргументов; копируется стандартный ввод

filecopy (stdin, stdout);

else

while (- - argc > 0)

if ((fp=fopen (* + + argv," r")) = = NULL)

{ fprintf (stderr, «%s: не могу открыть файл %sn», prog,* argv);

exit (1) ;

}

else { filecopy (fp, stdout);

fclose (fp);

}

if (ferror (stdout))

{ fprintf (stderr, «:%s: ошибка записи в stdoutn», prog);

exit (2);

}

exit (0);

}

Программа сигнализирует об ошибках двумя способами. Первый — сообщение об ошибке при помощи fprintf посылается в stderr с тем, чтобы оно попало на экран. Имя программы prog включено в сообщение для ясности источника ошибок.

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

Чтобы опустошить буфера, накопившие информацию для всех открытых файлов вывода, функция exit вызывает fclose.

Инструкция return (выражение) в главной программе эквивалентна обращению к функции exit (выражение).

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

Функция

int ferror (FILE*fp)

выдает ненулевое значение, если в файле fp была обнаружена ошибка.

Функция

int feof (FILE*)

возвращает ненулевое значение, если встретился конец указанного в аргументе файла.

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

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

union u_tag

{int ival;

float fval;

char * sval;

}r;

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

Синтаксис доступа к членам объединения следующий:

имя_объединения.член или указатель_на_объединение -> член т. е. в точности такой, как в структурах.

Объединения могут входить в структуры и массивы и наоборот. Например, в массиве структур

struct

{char *name;

int flags;

union

{int ival;

float fval;

char * sval;

} r;

}symtab [NSYM];

на ival ссылаются следующим образом

symtab[i]. r. ival

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

* symtab [i]. r. sval

symtab [i]. r. sval[o].

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

Инициализировать объединение можно только значением, имеющим тип его первого члена.

Интерфейс ввода-вывода.

Стандартный ввод-вывод. Форматный вывод.

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

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

Простейший механизм ввода — чтение одной литеры функцией getchar:

int getchar (void)

которая возвращает следующую литеру потока или, если обнаружен конец файла, EOF. Обычно значение EOF = -1.

Функция

int putchar (int)

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

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

В качестве примера рассмотрим программу lower, переводящую свой ввод на нижний регистр;

#include

#include

main ()

{int c;

while ((c= getchar ()) ! = EOF)

putchar (tolower (c));

return 0;

}

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

Форматный вывод.

Функция printf переводит внутренние значения в текст.

int printf (char*format, arg1, arg2,…)

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

Форматный стринг содержит два вида объектов: обычные литеры, которые впрямую копируются в выходной поток, и спецификации преобразования, каждая из которых вызывает преобразование и печать очередного аргумента printf. Любая спецификация преобразования начинается знаком % и заканчивается литерой — спецификатором. Между % и литеройспецификатором могут быть расположены такие элементы, как знак минус, число, точка, буква. Если за % не помещена литера — спецификатор, поведение функции printf будет не определено.

Функция sprintf выполняет те же преобразования, что и printf, но вывод запоминает в стринге

int sprintf (char*string, char*format, arg1, arg2,…)

Списки аргументов переменной длины. Форматный ввод Покажем как писать функции со списками аргументов переменной длины, причем такие, которые были бы переносимы. Для этого создадим упрощенную версию функции printf, которую назовем minprintf. Эта функция будет работать со стрингом, задающим формат, и аргументами; что же касается форматных преобразований, то они будут осуществляться при помощи стандартного printf.

Декларация стандартной функции printf выглядит так:

int printf (char*fmt,…),

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

void minprintf (char * fmt,…)

поскольку она не будет выдавать число литер.

Вся сложность в том, каким образом minprintf будет продвигаться вдоль списка аргументом — ведь у этого списка нет даже имени. Стандартный головной файл содержит набор макроопределений (va_start, va_arg, va_end), которые определяют, как шагать по списку аргументов.

Перечисленные средства образуют основу упрощенной версии printf.

#include

// minprintf: версия printf с переменным числом аргументов

void minprintf (char * fmt,…)

{va_list ap; //указатель на очередной безымянный аргумент, va_list — тип указателя

char *p, *sval;

int ival;

double dval;

va_start (ap, fmt); // макрос инициализирует ap первым безымянным аргументом

for (p=fmt; *p; p+ +)

{ if (*p≠'%')

{ putchar (*p);

continue;

}

switch (*+ +p)

{ case `d': ival= va_arg (ap, int); // макрос выдает очередной аргумент,

// а ар передвигает на следующий

printf («%d», ival);

break;

case`f':

dval=va_arg (ap, double); printf («%f», dval); break;

case`s': for (sval=va_arg (ap, char*);* sval; sval+ +)

putchar (* sval) ;

break;

default: putchar (*p);

break;

}

} va_end (ap); // очистка, когда все сделано

} 13.5 Форматный ввод (scanf)

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

int scanf (char * format, …);

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

Существует также функция sscanf, которая читает из стринга (а не из стандартного ввода).

int sscanf (char*string, char*format, arg1, arg2,…)

Функция sscanf просматривает string согласно формату format и рассылает полученные значения в arg1, arg2, и т. д. Последние должны быть указателями.

Для управления преобразованиями ввода формат обычно содержит такие спецификации, как: пробелы, обычные литеры, спецификации преобразования, знак *, число.

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

1 сен 2004

Обращение к scanf выглядит следующим образом:

int day, year;

char monthname [20];

scanf ( «%d%s%d», &day, monthname, &year);

Знак & перед monthname не нужен, так как имя массива есть указатель, что соответствует типу аргументов функции.

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

В стандартной библиотеке есть функция ввода fgets

char * fgets (char*line, int maxline, FILE*fp),

которая читает следующую строку ввода (включая и литеру новая строка) из файла fp в массив литер line, причем она может прочитать не более maxline -1 литер. Переписанная строка дополняется литерой `'.

Функция вывода fputs пишет стринг (который может и не заканчиваться литерой новая строка) в файл.

int fputs (char*line, FILE*fp).

Отличие библиотечных функций gets и puts от fgets и fputs в том, что они оперируют только стандартными файлами stdin и stdоut, и кроме того, gets выбрасывает последнюю литеру `n', а puts ее добавляет.

Директивы препроцессора. Именованные константы и макроопределения.

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

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

Средство # include позволяет, в частности, легко манипулировать наборами #define и деклараций. Любая строка вида

#include «имя-файла» или

# include <�имяфайла>

заменяется содержимым файла с именем имяфайла. Если имя-файла заключено в двойные кавычки, то, как правило, файл ищется среди исходных файлов программы; если такового не оказалось или имя — файла заключено в угловые скобки <�и >, то поиск осуществляется в системных библиотеках. Включаемый файл сам может содержать в себе строки #include.

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

Определение макроподстановки имеет вид:

# define имя замещающий-текст

Макроподстановка используется для простейшей замены: во всех местах, где встречается лексема имя, вместо нее будет помещен замещающий — текст. Имена в #define задаются по тем же правилам, что и имена обычных переменных. Замещающий текст может быть произвольным. Область действия имени в #define простирается от данного определения до конца файла.

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

#define max (A, B) ((A)>(B)? (A):(B))

Обращения к max будут вызывать только текстовую замену.

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

Так строка

x=max (p+q, r+s); будет заменена на строку

x=((p+q) > (r+s)? (p+q): (r+s));

Поскольку аргументы допускают любой вид замены, указанное определение max подходит для данных любого типа!

Имена можно «скрыть» от препроцессора с помощью #undef.

#undef getchar

int getchar (void) {…}

Как правило, это делается, чтобы макроопределение «перекрыть» настоящей функцией с тем же именем. Директива #undef обычно используется в паре с директивой #define.

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

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

#if константное выражение // проверяемое условие

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

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

#else

#endif

#elif

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

Первые три команды выполняют проверку условий, две следующие — позволяют определить диапазон действия проверяемого условия. Общая структура применения директив условной компиляции такова:

#if … текст1

#else текст2

#endif

Конструкция #else текст2 не обязательна. Текст1 включается в компилируемый текст только при истинности проверяемого условия, в противном случае (при наличии #else) на компиляцию передается текст2. Если директива #else отсутствует, то весь текст от # if до #endif при ложном условии опускается. Например, в результате выполнения директив:

# if 5+4

текст1

#endif

текст1 всегда будет включен в компилируемую программу.

В директиве #ifndef проверяется обратное условие — истинным считается неопределенность идентификатора, т. е. тот случай, когда идентификатор не был использован в команде #define или его определение было отменено командой #undef.

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

#define DE 1

#ifdef DE

printf («отладочная печать»);

#endif

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

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

// файл с именем filename

#ifndef FILE_NAME

// включаемый текст файла filename

#define FILE_NAME 1

#endif

Здесь FILE_NAME — зарезервированный для файла filename препроцессорный идентификатор, который не должен встречаться в других текстах программы.

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

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

Структура исходного текста с применением этой директивы такова:

#if

текст_ для_if

#elif выражение1

текст1

#elif выражение2

текст2

#else

текст_ для_случая_else

#endif

Препроцессор проверяет вначале условие в директиве #if, если она ложно (равно 0) — вычисляет выражение 1, если выражение 1 равно 0 — вычисляется выражение2 и т. д. Если все выражения ложны, то в компилируемый текст включается текст_для_случая_else. В противном случае, т. е. при появлении хотя бы одного истинного выражения (в #if или в #elif), начинает обрабатываться текст, расположенный непосредственно за этой директивой, а все остальные директивы не рассматриваются.

Основная литература — 1[143−154]. 3[146−153], 3[176−184].

Контрольные вопросы:

1. В чем существенная разница объединения от структуры?

2. В чем цель введения в программу объединения?

3. От чего зависит точный размер переменной объединения?

4. Какие операции применимы к объединениям?

5. Каким образом можно инициализировать переменную r?

6. Что должен содержать любой исходный Сифайл, использующий хотя бы одну функцию библиотеки ввода — вывода?

7. Каковы типичные случаи применения функции printf?

8. Какие два вида объектов содержит форматный стринг?

9. Каково назначение элементов расположенных между % и литерой — спецификатором?

10. Если типом аргумента arg функции printf является void*, то каков вид печати и от чего зависит представление данных?

11. Каким образом происходит продвижение по списку аргументов для функций с переменной длиной списка?

12. Назовите используемые макросы головного файла < stdarg. h>, позволяющие обрабатывать функции с переменными списком аргументов.

13. В каком случае функция scanf прекращает свою работу?

14. Перечислите литеры форматного стринга, соответствующие им вводимые данные и тип аргумента.

15. Какова одна из самых распространенных ошибок при применении функции scanf?

16. Какое полезное свойство несет в себе средство #include с точки зрения технологии разработки программы?

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

18. Что случится, если при определении #define square (x) x * x воспользоваться записью типа square (z+1)?

19. Будет ли ошибкой применение директивы #undef к идентификатору, который ранее не был определен (или действие его определения уже отменено) ?

20. Почему директива #undef обычно используется в паре с директивой # define?

21. Каким образом можно управлять самим ходом препроцессирования?

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

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

Лекция 14. Особенности программирования на языке Си++

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

Структура управления

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

Структура управления языка создают каркас программы. Эти структуры включают в себя принятие решений с помощью операторов if и else, организацию циклов с помощью операторов for, do, while, объединение операторов в группы, а также функции. Внимание, которое следует уделять употреблению этих структур в программе, зависит от того, насколько они облегчают понимание программы в целом, в какой последовательности производятся действия и что чем управляет. По возможности программа должна быть написана так, чтобы структуры передачи управления быстро и просто позволяли читателю понять, что, собственно, осуществляется с помощью программы.

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

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

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

Структура программы

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

Это — единственный путь повысить надежность программ, а также единственный способ прочитать и понять их. Функции и процедуры являются «модулями» или строительными блоками больших программ. Во многих языках они могут транслироваться по отдельности и эксплуатироваться независимо друг от друга. Большие модули, если они выполняют слишком многочисленные или слишком разные функции, становятся слишком специализированными для широкого применения и их сложно эксплуатировать.

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

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

— единственный способ написания и сопровождения больших программ — представление их в виде совокупности небольших функций, организованных в виде модулей;

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

— каждый модуль должен взаимодействовать только с небольшой группой непосредственных соседей;

— в структуре программы должны находить отражение обрабатываемые данные.

Операции распределения памяти В языке C++ для управления динамической памятью введены операции new и delete (для массивов new [ ] и delete [ ]). В С для этого применялись в C++, однако новые операции имеют некоторые преимущества.

Переопределение операций new и delete

Стандартные (глобальные) версии операций new и delete можно переопределить или перегрузить, чтобы придать им некоторые дополнительные свойства или обеспечить возможность передачи им дополнительных аргументов. Это бывает полезно при отладке, когда требуется проследить все выделения и освобождения динамической памяти:

#include

#include

/ / Переопределение операций new и delete.

void* operator new (size_t size)

{

printf («%u bytes requested. n», size);

return malloc (size);

void operator delete (void *ptr)

{

printf («Delete performed. n») ;

free (ptr) ;

}

// Перегрузка new для выдачи дополнительной информации.

void* operator new (size t size, char *file, int line)

printf («%u bytes requested in file %s line %d.n», size, file, line) ;

return malloc (size);

}

int main (void) {

double *dptr = new double; // Вызывает новую new.

*dptr = 3.14 159 265;

delete dptr; // Вызывает новую delete.

// Вызов перегруженной new.

dptr = new (_FILE_, _LINE_) double;

*dptr = 1.0;

delete dptr;

return 0;

}

Примечание Здесь используется механизм определения функций-операций C++. В этом языке можно переопределить или перегрузить практически любое обозначение операции, чтобы, например, можно было применять стандартные знаки арифметических операций к объектам созданных пользователем типов. Об этом мы будем говорить в следующих главах.

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

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

Примечание Перегруженная версия new может быть, кстати, определена таким образом, что она не будет выделять никакой новой памяти, а будет использовать уже существующий объект. Адрес, где нужно разместить «новый» объект, должен быть одним из дополнительных параметров функции-операции. Эта форма new известна как синтаксис размещения (placement syntax).

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

Операция delete имеет тип void, а ее параметр — void*.

Обработка ошибок В C++ имеется библиотечная функция 5et_new_handler ().Он будет вызываться при любой ошибке выделения памяти.

#include

#include

#include

void MyHandler (void)

{

prir-tf («No memory! n»);

exit (l) ;

}

int main (void) {

set_new_handler (MyHandler); //Установка обработчика.

return 0;

}

Обработчик ошибок new должен:

· либо возвратить управление, освободив память;

· либо вызвать abort () или exit ();

· либо выбросить исключение bad_alloc или производного класса Дополнительная литература — 12[10−153], 13[60−76].

Контрольные вопросы:

1. Каким образом правильно записывать отдельные операторы?

2. Каковы управляющие структуры программы и как передача управления определяется с помощью циклов и операторов принятия решения?

3. Каким образом необходимо представить данные, чтобы упростить программу?

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

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

Лекция 15. Объектно-ориентированное программирование: основы Объектно-ориентированный подход включает в себя все стилевые усовершенствования, базирующиеся на понятии «объект». Всякий объект характеризуется своими свойствами и поведением. Объектно-ориентированный подход можно применять как к программированию, так и анализу и дизайну (проектированию программ).

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

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

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

Основная литература — 5[387−432].

Дополнительная литература — 14[27−194].

Контрольные вопросы:

1. Три основных принципа ООП?

2. Каким образом в ООП происходит управление работой программы?

3. Что такое объект, его свойства?

4. Каким образом в ООП происходит организация и обработка данных?

5. Чем отличается язык Си от объектно-ориентированного С + +?

Показать весь текст
Заполнить форму текущей работой