Главным качеством языка C, которое делает его именно языком системного программиста, является то, что «C — это язык относительно «низкого уровня»... Это означает, что C имеет дело с объектами того же вида, что и большинство ЭВМ, а именно, с символами, числами и адресами. Они могут объединяться и пересылаться посредством обычных арифметических и логических операций, осуществляемых реальными ЭВМ.» [2]. Система программирования C при представлении данных не вносит никаких дополнительных структур памяти, которые не были бы «видны» программисту. Так, например, внутреннее представление массивов в языке C полностью совпадает с внешним: массив — это только последовательность слотов в памяти. Отсутствие специального дескриптора массива, с одной стороны, делает невозможным контроль выхода индексов за допустимое границы, но с другой, уменьшает объем памяти программы и увеличивает ее быстродействие за счет отсутствия в памяти дескриптора и команд проверки индекса при каждом обращении к элементу массива. Это общий принцип C-программ: программист имеет максимальные возможности для разработки эффективных программ, но вся ответственность за их правильность ложится на него самого. Поэтому отладка программ на языке C — непростой процесс, C-программы чаще «зависают» и выдают результаты, которые не всегда воспроизводятся и которые труднее объяснить, чем программы на других языках.
Чрезвычайно важным свойством языка C, которого нет в других языках, является адресная арифметика. Над данными типа «указатель» возможны арифметические операции, причем в них могут учитываться размеры тех объектов, которые адресуются указателем. Другое свойство указателей — их явная связь с конструкциями интеграции данных (массивы, структуры, объединения) и возможность подмены операций индексации и квалификации операциями адресной арифметики. За счет указателей программист имеет возможность удобным для себя способом структурировать адресное пространство программы и гибко изменять это структурирование.
Свойством, которое вытекает из общих принципов построения языка C, является слабая защита типов. В языках с сильной защитой типов (Pascal) для каждого типа данных определен набор доступных операций и компилятор запрещает применение к типу непредусмотренных для него операций и смешивание в выражениях данных разных типов. В C определен богатый набор правил преобразования типов по умолчанию, поэтому почти любая операция может быть применена к почти любому типу данных и выражения могут включать данные самых разных типов.
Еще некоторые средства языка не ориентированы непосредственно на низкоуровневое системное программирование, но могут быть очень полезны при разработке системных программ:
u обязательной составной частью языка является препроцессор. C не поддерживает сложных структур данных, но позволяет программисту определять свои типы. Включение в программу описания таких типов средствами препроцессора позволяет обеспечить однозначную интерпретацию типов во всех модулях сложного программного изделия;
u процедурно-ориентированный язык C вместе с тем представляет все кодовые составляющие программы в виде функций. Это дает возможность применять язык C и як инструмент функционально-ориентированного программирования, как, например, язык LISP.
Важное качество языка C — высокая эффективность объектных кодов C-программы как по быстродействию, так и по объему памяти. Хотя это качество обеспечивается не столько свойствами самого языка, сколько свойствами системы программирования, традиция построения систем программирования C именно такова, что они обеспечивают большую, эффективность, чем, например, Pascal. Это связано еще и с «родословной» языков. Pascal возник как алгоритмический язык, предназначенный в своей первой версии не для написания программ, а для описания алгоритмов. Отсюда Pascal-трансляторы строились и строятся как синтаксически-ориентированные трансляторы характерно компилирующего типа: транслятор выполняет синтаксический анализ программы в соответствии с формально представленными правилами, а объектные коды формируются в основном в виде обращений к библиотечным процедурам, которые реализуют элементарные функции. Язык C ведет свое происхождение от языка BCPL, который был языком Макроассемблера. Отсюда и объектный код C-программы строится як последовательность машинных команд, которая оптимизируется для каждого конкретного выполнения данной функции.
:
Порядок выполнения работ
Для выполнения всех лабораторных работ предлагается единый порядок, предусматривающий следующие шаги.
u Ознакомиться с постановкой задачи и исходными данными. Определить вариант индивидуального задания.
u Сконструировать структуру программы.
u Составить текст программы.
u Набрать текст программы.
u Выполнить компиляцию программы.
u Провести анализ и исправление обнаруженных синтаксических ошибок в тексте программы.
u Получить решение (изображение) и, в случае обнаружения логических ошибок, определить и устранить их.
:
Содержание отчета
Отчет оформляется по каждой лабораторной работе и состоит из следующих разделов.
u Лекция лабораторной работы.
u Цель работы.
u Индивидуальное задание.
u Описание структур данных и алгоритмов
u Результаты работы программы.
u Интерпретация результатов.
:
Лабораторная работа №1.
Работа с символьными строками
Цель работы
Получение практических навыков в работе с массивами и указателями языка C, обеспечение функциональной модульности
Темы для предварительного изучения
u Указатели в языке C.
u Представление строк.
u Функции и передача параметров.
Постановка задачи
По индивидуальному заданию создать функцию для обработки символьных строк. За образец брать библиотечные функции обработки строк языка C, но не применять их в своей функции. Предусмотреть обработку ошибок в задании параметров и особые случаи. Разработать два варианта заданной функции — используя традиционную обработку массивов и используя адресную арифметику.
Индивидуальные задания
Функция Copies(s,s1,n)
Назначение: копирование строки s в строку s1 n раз
Функция Words(s)
Назначение: подсчет слов в строке s
Функция Concat(s1,s2)
Назначение: конкатенация строк s1 и s2 (аналогичная библиотечная функция C — strcat)
Функция Parse(s,t)
Назначение: разделение строки s на две части: до первого вхождения символа t и после него
Функция Center(s1,s2,l)
Назначение: центрирование — размещение строки s1 в середине строки s2 длиной l
Функция Delete(s,n,l)
Назначение: удаление из строки s подстроки, начиная с позиции n, длиной l (аналогичная библиотечная Функция есть в Pascal).
Функция Left(s,l)
Назначение: выравнивание строки s по левому краю до длины l.
Функция Right(s,l)
Назначение: выравнивание строки s по правому краю до длины l.
Функция Insert(s,s1,n)
Назначение: вставка в строку s подстроки s1, начиная с позиции n (аналогичная библиотечная функция есть в Pascal).
Функция Reverse(s)
Назначение: изменение порядка символов в строке s на противоположный.
Функция Pos(s,s1)
Назначение: поиск первого вхождения подстроки s1 в строку s (аналогичная функция C — strstr).
Функция LastPos(s,s1)
Назначение: поиск последнего вхождения подстроки s1 в строку s.
Функция WordIndex(s,n)
Назначение: определение позиции начала в строке s слова с номером n.
Функция WordLength(s,n)
Назначение: определение длины слова с номером n в строке s.
Функция SubWord(s,n,l)
Назначение: выделение из строки s l слов, начиная со слова с номером n.
Функция WordCmp(s1,s2)
Назначение: сравнение строк (с игнорированием множественных пробелов).
Функция StrSpn(s,s1)
Назначение: определение длины той части строки s, которая содержит только символы из строки s1.
Функция StrCSpn(s,s1)
Назначение: определение длины той части строки s, которая не содержит символы из строки s1.
Функция Overlay(s,s1,n)
Назначение: перекрытие части строки s, начиная с позиции n, строкой s1.
Функция Replace(s,s1,s2)
Назначение: замена в строке s комбинации символов s1 на s2.
Функция Compress(s,t)
Назначение: замена в строке s множественных вхождений символа t на одно.
Функция Trim(s)
Назначение: удаление начальных и конечных пробелов в строке s.
Функция StrSet(s,n,l,t)
Назначение: установка l символов строки s, начиная с позиции n, в значение t.
Функция Space(s,l)
Назначение: доведение строки s до длины l путем вставки пробелов между словами.
Функция Findwords(s,s1)
Назначение: поиск вхождения в строку s заданной фразы (последовательности слов) s1.
Функция StrType(s)
Назначение: определение типа строки s (возможные типы — строка букв, десятичное число, 16-ричное число, двоичное число и т.д.).
Функция Compul(s1,s2)
Назначение: сравнение строк s1 и та s2 с игнорированием различий в регистрах.
Функция Translate(s,s1,s2)
Назначение: перевод в строке s символов, которые входят в алфавит s1, в символы, которые входят в алфавит s2.
Функция Word(s)
Назначение: выделение первого слова из строки s.
Примечание: под «словом» везде понимается последовательность символов, которая не содержит пробелов.
:
Пример решения задачи
Индивидуальное задание
Функция substr(s,n,l)
Назначение: выделение из строки s подстроки, начиная с позиции n, длиной l.
Описание метода решения
Символьная строка в языке C представляется в памяти как массив символов, в конце которого находится байт с кодом 0 — признак конца строки. Строку, как и любой другой массив можно обрабатывать либо традиционным методом — как массив, с использованием операции индексации, либо через указатели, с использованием операций адресной арифметики. При работе со строкой как с массивом нужно иметь в виду, что длина строки заранее неизвестна, так что циклы должны быть организованы не со счетчиком, а до появления признака конца строки.
Функция должна реализовывать поставленную задачу — и ничего более. Это означает, что функцию можно будет, например, перенести без изменений в любую другую программу, если спецификации функции удовлетворяют условиям задачи. Это также означает, что при ошибочном задании параметров или при каких-то особых случаях в их значениях функция не должна аварийно завершать программу или выводить какие-то сообщения на экран, но должна возвращать какое-то прогнозируемое значение, по которому та функция, которая вызвала нашу, может сделать вывод об ошибке или об особом случае.
Определим состав параметров функции:
int substr (src, dest, num, len);
где
u src — строка, с которой выбираются символы;
u dest — строка, в которую записываются символы;
u num — номер первого символа в строке src, с которого начинается подстрока (нумерация символов ведется с 0);
u len — длина выходной строки.
Возможные возвращаемые значения функции установим: 1 (задание параметров правильное) и 0 (задание не правильное). Эти значения при обращениях к функции можно будет интерпретировать как «истина» или «ложь».
Обозначим через Lsrc длину строки src. Тогда возможны такие варианты при задании параметров:
u num+len <= Lsrc — полностью правильное задание;
u num+len > Lsrc; num < Lsrc — правильное задание, но длина выходной строки будет меньше, чем len;
u num >= Lsrc — неправильное задание, выходная строка будет пустой;
u num < 0 или len <= 0 — неправильное задание, выходная строка будет пустой.
Заметим, что интерпретация конфигурации параметров как правильная/неправильная и выбор реакции на неправильное задание — дело исполнителя. Но исполнитель должен строго выполнять принятые правила. Возможен также случай, когда выходная строка выйдет большей длины, чем для нее отведено места в памяти. Однако, поскольку нашей функции неизвестен размер памяти, отведенный для строки, функция не может распознать и обработать этот случай — так же ведут себя и библиотечные функции языка C.