Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Основы разработки программного обеспечения на примере языка С. Учебное пособие для СПО
.pdf
Язык программирования Си
Язык Си подобен обоюдоострому лезвию, при помощи которого легко
создать как изысканное блюдо, так и кровавое месиво.
Брайан Керниган
Язык Си был создан в 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
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
