Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Основы разработки программного обеспечения на примере языка С. Учебное пособие для СПО

.pdf
Скачиваний:
0
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
☆
Язык программирования Си
Язык Си подобен обоюдоострому лезвию, при помощи которого легко создать как изысканное блюдо, так и кровавое месиво.
Брайан Керниган
Язык Си был создан в 1972 г. Деннисом Ритчи в ходе разработки операционной системы (ОС) Unix. Практически вся ОС была написана на Си. Большие выразительные возможности и легкость расширения способствовали быстрому распространению этого языка. Компиляторы для языка Си доступны практически для всех существующих на данный момент ОС и платформ.
В отличие от большинства языков высокого уровня (Ада, Алгол и др.), которые начинали применять только после принятия соответствующих стандартов на них, язык Си был создан как рабочий инструмент эффективного кодирования и не претендовал на общественное использование. Спецификация языка была определена в книге Кернигана Б. и Ритчи Д. (перевод на русский язык [8
]). Эта книга оставалась неформальным стандартом на язык Си вплоть до 1989 г., когда был принят стандарт ANSI. На данный момент существует еще ISO-стандарт на этот язык.
В качестве отличительных особенностей языка Си можно выделить:
препроцессорные директивы; необходимость определять переменные, но возможность это делать в любом месте (до первого использования переменной); использование фигурных скобок {} для структуризации текста программы; обязательное наличие функции main()
3.1. Типы в языке Си
Как и в любом языке программирования, в Си определены базовые типы и набор механизмов для задания произвольных пользовательских
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
61
типов на основе базовых.
Базовые типы
В качестве базовых в языке Си определены следующие типы.
Целочисленные:
char - целый длиной не менее 8 бит (интерпретируется обычно как литера); short /short int/ - короткий целый; int - целый; long - длинный целый.
Для всех целочисленных типов можно определить, будут они знаковые или нет. Это делается прибавлением слова unsigned или signed перед названием типа (по умолчанию - signed).
В ранних компьютерах целые числа представлялись своим модулем и отдельным знаковым битом. В первых машинах размещали знак в младшем разряде, а когда стала возможной параллельная обработка, знак переместили в старший разряд, аналогично общепринятой бумажной нотации. Однако использование представления "знак и модуль" приводило к усложнению структуры вычислителя, поскольку для положительных и отрицательных чисел требовались разные схемы обработки.
Более удачным оказалось представление отрицательных чисел в виде дополнения, поскольку одна и та же схема могла выполнять и сложение, и вычитание. Некоторые разработчики выбирали дополнение до единицы, когда получалось из путем простого инвертирования всех бит (так называемый обратный код). Другие выбирали дополнение до двойки, когда получалось путем инвертирования всех бит и прибавлением единицы. Недостатком первого способа было наличие двух форм представления нуля (0...0 и 1...1). Это неприятно, особенно если доступные инструкции сравнения являются неадекватными. Различия этих форм показаны в табл. 3.1
.
Таблица 3.1. Представление в прямом, обратном и дополнительных
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
62
Таблица 3.1. Представление в прямом, обратном и дополнительных
кодах
Десятичное
представление
Прямой код
Дополнительный
код
Обратный
код
Знак Модуль
2 0 010 010 010
1 0 001 001 001
0 0 000 000 000 или 111
-1 1 001 111 110
-2 1 010 110 101
Сегодня во всех компьютерах используется арифметика с дополнением до двойки, обычно называемая просто дополнительным кодом. Как мы видим, у отрицательных чисел старший разряд равен 1. Добавление спецификации unsigned означает, что весь код следует воспринимать как модуль числа. При этом нижняя строчка таблицы, соответствующая -2 в десятичной системе и представляющая в дополнительном коде 110, будет обрабатываться как десятичное число шесть. Естественно, при signed-спецификации этот же код будет обрабатываться как - 2.
Числа с дробной частью можно представлять в формах с фиксированной и плавающей точкой. В современной аппаратуре для вещественных чисел обычно поддерживается арифметика с плавающей точкой, т.е. число представляется двумя целыми числами - порядком
и мантиссой , так что . Это позволяет значительно
расширить диапазон представляемых в машине чисел.
Вещественные:
float - вещественный одинарной точности; double - вещественный двойной точности; long double - вещественный максимальной точности.
Недостатком такого представления является то, что та же память теперь делится между двумя целыми числами: мантиссой числа и его порядком. Это приводит к сокращению числа разрядов мантиссы, а следовательно, к меньшей ее точности. Более того, практически одно и то же число может теперь быть представлено разной комбинацией разрядов: чуть большим или чуть меньшим значением. Т.е. сравнение
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
63
чисел надо производить с учетом некоторого диапазона чувствительности. Стоит при этом помнить еще и о том, что складывая очень большие и очень маленькие вещественные числа из различных диапазонов, каждое из которых помещается в машинное представление, мы рискуем потерять младшие разряды мантиссы, а значит, и значение малой добавки.
Разработчики аппаратуры шли на различные ухищрения для смягчения этого свойства: отказывались от хранения старшего разряда мантиссы для увеличения ее точности, переходили к удвоенной разрядной сетке, увеличивали основание порядка до 8 и даже до 16. Однако проблемы округления оставались. Известны случаи, когда в подобных машинах можно было найти такие значения x и у, что для некоторого малого положительного ? выполнялось соотношение
, т.е. умножение утрачивало свойство
монотонности.
Соглашения об именах
Мы уже говорили, обсуждая программную документацию, что для текстов программ характерно однократное написание и многократное прочтение. Весьма значимым при чтении текста программы является отделение имен одного класса объектов от других. Проще всего для этого ввести некоторые соглашения об именах, например имена типов и констант писать ТОЛЬКО заглавными буквами, начинать определение имени без знаковых типов и буквы U (unsigned), перечислимых типов - с буквы E (enumeric) и тому подобное. Для этих целей можно воспользоваться следующими определениями:
typedef char CHAR; /* general text. Range 0..127 */ typedef unsigned char UCHAR; /* extended text. Range 0..255 */ typedef unsigned short BOOL; /* Time efficient Boolean */ typedef unsigned har TBOOL; /* Tiny (space efficient) Boolean */ typedef char TINY; /* 8 bit. Range (-2^7-1)..(+2^7) */ typedef unsigned char UTINY; /* 8 bit. Range (0)..(+2^8) */ typedef unsigned char TINYBITS; /* 8 bit. For bit manipulation use */ typedef short SHORT; /* 16 bit. Range (-2^15-1)..(+2^15) */ typedef unsigned short USHORT; /* 16 bit. Range (0)..(+2^16) */
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
64
typedef unsigned short SHORTBITS; /* 16 bit. For bit manipulation use */ typedef long LONG; /* 32 bit.Range (-2^31-1)..(+2^31)*/ typedef unsigned long ULONG; /* 32 bit.Range (0)..(+2^32) */ typedef unsigned long LONGBITS; /* 32 bit.For bit manipulation use */ typedef int INT; /* Natural machine size integer */
Как мы видим, один и тот же тип языка Си, например unsigned char, может быть многократно переопределен под различными именами в зависимости от целей его использования.
Типы, определяемые пользователем
Программисту позволяется вводить в программу свои собственные средства кодировки, приписывая каждому значению собственное имя (идентификатор). Для этого в языке Си введен перечислимый тип.
Перечислимые:
enum - перечисление констант, например:
enum { Yellow=5, Cyan, Magenta};
или, если следовать правилу использования в именах констант только заглавных букв:
enum { YELLOW=5, CYAN, MAGENTA};
Здесь после первого элемента стоит знак "=" и за ним число, это число определяет, с какого значения идет нумерация перечисленных констант (следующая константа будет иметь значение на единицу больше). Если не указывать это начальное число, то нумерация констант будет идти от нуля. Таким образом, к перечисленным константам можно обращаться по их номерам, а можно задать и полностью свою систему обозначений:
typedef enum {
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
65
START END FLAG = 0x0001, /* Event Flag 0 * / TEST END FLAG = 0x0002, /* Event Flag 1 * / SYS INIT END = 0x0004, /* Event Flag 2 */ CODEC CODE DOWNLOADED = 0x0008, /* Event Flag 3 */ MODEM CODE DOWNLOADED = 0x0010 /* Event Flag 4 */ } E_RUN_FLAG;
Такое определение перечислимого типа позволяет закодировать значения единицы в разрядах с нулевого по четвертый соответственно. Важно только помнить, что константы типа перечисляются ВСЕГДА в порядке возрастания их значений. И при соблюдении ранее указанных правил наименований если вы где-либо в программе встретите строку определения:
E_RUN_FLAG My_Indicator;
то вы легко догадаетесь, что переменная My_Indicator имеет перечислимый тип.
Ключевое слово void определяет отсутствие типа и используется для "нейтрализации" значения, возвращаемого функцией [9
].
Сложные типы
В языке Си определены следующие механизмы для задания сложных типов:
массивы; структуры; указатели; строки; объединения.
Массивы
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
66
Массивы представляют собой однородную последовательность элементов, каждый из которых характеризуется порядковым номером в данной последовательности. Номер элемента указывается вслед за идентификатором последовательности (идентификатором массива) в квадратных скобках: My_Array[i] , Your_Array[5] и т.п. Самый первый элемент массива имеет порядковый номер 0 (нуль).
Объявление массива в программе в простейшем случае может выглядеть так:
int My_Vector[3] ;
где константа 3 указывает, что последовательность, представляемая массивом, содержит три элемента: My_Vector [0], My_Vector
[1], My Vector [2].
При этом следует помнить, что упоминание в программе самого идентификатора массива практически эквивалентно указанию адреса его нулевого элемента.
Записи
Записи (или структуры) представляют собой набор полей, объединенных вместе. Можно работать и с каждым полем отдельно, и со всеми полями, как с записью, вместе.
Вот пример задания структуры:
struct goods { char * name; long price; int volume; };
Здесь goods - имя структурного типа. В этом случае объявление переменной food такого типа будет иметь вид:
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
67
struct goods food;
Если же объявить структуру как:
typedef struct { char * name; long price; int volume; } GOODS;
то можно будет объявлять переменные, используя только имя типа:
GOODS food;
Язык Си разрешает присваивание структур (в отличие от присваивания массивов, которое может быть выполнено только поэлементным присваиванием).
При объявлении переменной структурного типа можно проинициализировать ее значение при помощи следующей конструкции:
GOODS food = {"milk", 25.6, 1} ;
Для работы с полями в языке используется составное имя с точкой:
food.price = 27.5;
Указатели
Указатели в языке Си объявляются при помощи символа "*":
int * pInt;
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
68
Здесь переменная pInt - это указатель на int или, проще, адрес переменной целого типа. Доступ к значению, хранящемуся по адресу указателя, производится через разыменование:
*pInt=6;
Забавно, что если написать обе строки (объявление указателя и присваивание) подряд, то получится синтаксически правильный фрагмент, но по смыслу он ошибочен. Объявлением указателя мы выделили память под сам указатель, т.е. под адрес некоторого объекта. Однако память под сам объект не выделена, и поэтому записывать значение 6 некуда.
При помощи оператора "&" можно получить адрес уже существующей переменной. Следующий пример демонстрирует, как можно установить указатель pint на переменную varlnt типа int:
int * pint; int varint; pint = &varInt;
После этого уже можно выполнять присваивание *plnt=6;. Теперь значение 6 попадет в память переменной varint.
Указатель, "не указывающий никуда", в языке Си называется Null. Это единственная неарифметическая константа. Иногда она представлена как NULL.
Сами по себе указатели не являются сложными типами с точки зрения обращения к ним. Их значением является адрес некоторого программного объекта. Как правило, они указывают на некоторый тип, который в свою очередь уже может иметь сложную составную структуру. Специфика работы с указателями заключается в том, что, например, префиксная или постфиксная операция ++ будет изменять значение указателя на 1, 2, 4 или некоторую другую величину в зависимости от длины (количеств байт, отведенных для представления) того типа, на который указывает этот указатель.
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
69
Еще одной и наиболее важной проблемой указателей является несогласованность времени жизни указателя (адреса объекта) и программного объекта, на который он указывает.
Например, функция сохраняет адрес некоторой своей локальной переменной в глобальном указателе. По выходу из функции памяти объекта уже нет. Она может быть перераспределена каким угодно образом, а адрес, где "все это" лежало до сих пор, существует и может быть использован для обращения.
Ошибки подобного рода очень трудно обнаружить, поскольку до некоторого времени поведение программы с дефектами такого вида может быть весьма правдоподобным. Это происходит до тех пор, пока "отданная" память никем не переопределялась, а потом такая программа "вдруг" перестает работать.
Строки
Для работы со строками в Си не введено специального типа, поэтому для представления элементов строки используется символьный тип. Принято, что строка - это массив символов (байт), заканчивающийся нулевым байтом ('\0'). Соответственно, пустая строка начинается именно с этого символа (байта с нулевым кодом).
Задать значение массиву символов простым присваиванием нельзя, необходимо пользоваться инициализацией, функциями ввода данных или функциями копирования. В разделе 3.5 рассмотрен пример подобной функции My_Strcpy. Следует помнить, что строковые константы в Си выделяются в двойных апострофах, а символьные, занимающие один байт, - в одиночных.
Объединения
Объединение описывает переменную, которая может иметь любой тип из некоторого множества типов, т.е. все они имеют собственные имена, но используют общую память.
Пример определения типа объединения:
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
70
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]