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

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

Основные понятия языка Си.

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

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

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

1. Они должны начинаться с буквы латинского алфавита (а,...,z, А,...,Z) или с символа подчеркивания (_).

2. В них могут использоваться буквы латинского алфавита, символ подчеркивания и цифры (0,...,9). Использование других символов в идентификаторах запрещено.

3. В языке Си буквы нижнего регистра (а,...,z), применяемые в идентификаторах, отличаются от букв верхнего регистра (А,...,Z). Это означает, что следующие идентификаторы считаются разными: name, NaMe, NAME и т.д.

4. Идентификаторы могут иметь любую длину, но воспринимается и используется для различения объектов (функций, переменных, констант и т.д.) только часть символов. Их число меняется для разных систем программирования, но в соответствии со стандартом ANSI C не превышает 32 (в Си++ это ограничение снято). Если длина идентификатора установлена равной 5, то имена count и counter будут идентичны, поскольку у них совпадают первые пять символов.

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

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

Типы данных

Программы оперируют с различными данными, которые могут быть простыми и структурированными. Простые данные - это целые и вещественные числа, символы и указатели (адреса объектов в памяти). Целые числа не имеют, а вещественные имеют дробную часть. Структурированные данные - это массивы и структуры; они будут рассмотрены ниже.

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

* char - символьный;

* int - целый;

* float - вещественный;

* double - вещественный двойной точности;

* void - не имеющий значения.

Дадим им краткую характеристику:

1. Переменная типа char имеет размер 1 байт, ее значениями являются различные символы из кодовой таблицы, например: 'ф', ':', 'j' (при записи в программе они заключаются в одинарные кавычки).

2. Размер переменной типа int в стандарте языка Си не определен. В большинстве систем программирования размер переменной типа int соответствует размеру целого машинного слова. Например, в компиляторах для 16-разрядных процессоров переменная типа int имеет размер 2 байта. В этом случае знаковые значения этой переменной могут лежать в диапазоне от -32768 до 32767.

3. Ключевое слово float позволяет определить переменные вещественного типа. Их значения имеют дробную часть, отделяемую точкой, например: -5.6, 31.28 и т.п. Вещественные числа могут быть записаны также в форме с плавающей точкой, например: -1.09e+4. Число перед символом "е" называется мантиссой, а после "е" - порядком. Переменная типа float занимает в памяти 32 бита. Она может принимать значения в диапазоне от 3.4е-38 до 3.4e+38.

4. Ключевое слово double позволяет определить вещественную переменную двойной точности. Она занимает в памяти в два раза больше места, чем переменная типа float (т.е. ее размер 64 бита). Переменная типа double может принимать значения в диапазоне от 1.7e-308 до 1.7e+308.

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

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

* unsigned

* signed * short

* long Модификаторы записываются перед спецификаторами типа, например: unsigned char. Если после модификатора опущен спецификатор, то компилятор предполагает, что этим спецификатором является int. Таким образом, следующие строки:

long а; long int а;

являются идентичными и определяют объект а как длинный целый. Табл. 1 иллюстрирует возможные сочетания модификаторов (unsigned, signed, short, long) со спецификаторами (char, int, float и double), а также показывает размер и диапазон значений объекта (для 16-разрядных компиляторов).

Тип Размер в байтах (битах) Интервал изменения char 1 (8) от -128 до 127 unsigned char 1 (8) от 0 до 255 signed char 1 (8) от -128 до 127 int 2 (16) от -32768 до 32767 unsigned int 2 (16) от 0 до 65535 signed int 2 (16) от -32768 до 32767 short int 2 (16) от -32768 до 32767 unsigned short int 2 (16) от 0 до 65535 signed short int 2 (16) от -32768 до 32767 long int 4 (32) от -2147483648 до 2147483647 unsigned long int 4 (32) от 0 до 4294967295 signed long int 4 (32) от -2147483648 до 2147483647 float 4 (32) от 3.4Е-38 до 3.4Е+38 double 8 (64) от 1.7Е-308 до 1.7Е+308 long double 10 (80) от 3.4Е-4932 до 3.4Е+4932

Переменные и константы

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

int a, b, c;

char x, y; В языке различают понятия объявления переменной и ее определения. Объявление устанавливает свойства объекта: его тип (например, целый), размер (например, 4 байта) и т.д. Определение наряду с этим вызывает выделение памяти (в приведенном примере дано определение переменных).

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

float a;

float b; Переменные в языке Си могут быть инициализированы при их определении:

int a = 25, h = 6;

char g = 'Q', k = 'm';

float r = 1.89;

long double n = r*123;

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

int a; /* Определение глобальной переменной */

int function (int b, char c); /* Объявление функции (т.е. описание

ее заголовка)*/

void main (void)

{ //Тело программы

int d, e; //Определение локальных переменных

float f; //Определение локальной переменной

...

} Наряду с переменными в языке существуют следующие виды констант:

* вещественные, например 123.456, 5.61е-4. Они могут снабжаться суффиксом F (или f), например 123.456F, 5.61e-4f;

* целые, например 125;

* короткие целые, в конце записи которых добавляется буква (суффикс) H (или h), например 275h, 344H;

* длинные целые, в конце записи которых добавляется буква (суффикс) L (или l), например 361327L;

* беззнаковые, в конце записи которых добавляется буква U (или u), например 62125U;

* восьмеричные, в которых перед первой значащей цифрой записывается нуль (0), например 071;

* шестнадцатеричные, в которых перед первой значащей цифрой записывается пара символов нуль-икс (0x), например 0x5F;

* символьные - единственный символ, заключенный в одинарные кавычки, например 'О', '2', '.' и т.п. Символы, не имеющие графического представления, можно записывать, используя специальные комбинации, например \n (код 10), \0 (код 0). Эти комбинации выглядят как два символа, хотя фактически это один символ. Так же можно представить любой двоичный образ одного байта: '\NNN', где NNN - от одной до трех восьмеричных цифр. Допускается и шестнадцатеричное задание кодов символов, которое представляется в виде: '\х2В', '\хЗ6' и т.п.;

* строковые - последовательность из нуля символов и более, заключенная в двойные кавычки, например: "Это строковая константа". Кавычки не входят в строку, а лишь ограничивают ее. Строка представляет собой массив из перечисленных элементов, в конце которого помещается байт с символом '\0'. Таким образом, число байтов, необходимых для хранения строки, на единицу превышает число символов между двойными кавычками;

* константное выражение, состоящее из одних констант, которое вычисляется во время трансляции (например: а=60+301);

* типа long double, в конце записи которых добавляется буква L (или l), например: 1234567.89L.

Операции языка Си

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

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

Таблица 2

Знак операции Назначение операции ( ) Вызов функции [ ] Выделение элемента массива . Выделение элемента записи -> Выделение элемента записи ! Логическое отрицание ~ Поразрядное отрицание - Изменение знака ++ Увеличение на единицу -- Уменьшение на единицу & Взятие адреса * Обращение по адресу (тип) Преобразование типа (т.е. (float) a) sizeof( ) Определение размера в байтах * Умножение / Деление % Определение остатка от деления + Сложение - Вычитание > Сдвиг вправо Больше, чем >= Больше или равно = = Равно != Не равно & Поразрядное логическое "И" ^ Поразрядное исключающее "ИЛИ" | Поразрядное логическое "ИЛИ" && Логическое "И" || Логическое "ИЛИ" ?: Условная (тернарная) операция = Присваивание +=, - =, *=, /=, %=, >=, &=, |=, ^= Составные операции присваивания (например, а *= b

(т.е. a = a * b) и т.д.) , Операция запятая Для исключения путаницы в понятиях "операция" и "оператор", отметим, что оператор - это наименьшая исполняемая единица программы. Различают операторы выражения, действие которых состоит в вычислении заданных выражений (например: a = sin(b)+c; j++;), операторы объявления, составные операторы, пустые операторы, операторы метки, цикла и т.д. Для обозначения конца оператора в языке Си используется точка с запятой. Что касается составного оператора (или блока), представляющего собой набор логически связанных операторов, помещенных между открывающей ({) и закрывающей (}) фигурными скобками ("операторными скобками"), то за ним точка с запятой не ставится. Отметим, что блок отличается от составного оператора наличием определений в теле блока.

Охарактеризуем основные операции языка Си. Сначала рассмотрим одну из них - операцию присваивания (=). Выражение вида

х = у;

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

x = y = z = 100;

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

1. Арифметические операции.

2. Логические операции и операции отношения.

3. Операции с битами.

Арифметические операции задаются следующими символами (табл. 2): +, -, *, /, %. Последнюю из них нельзя применять к переменным вещественного типа. Например:

a = b + c;

x = y - z;

r = t * v;

s = k / l;

p = q % w;

Логические операции отношения задаются следующими символами (см. табл. 2): && ("И"), || ("ИЛИ"), ! ("НЕ"), >, >=, > (сдвиг вправо), & (поразрядное "И"), ^ (поразрядное исключающее "ИЛИ"), | (поразрядное "ИЛИ").

Примеры: если a = 0000 1111 и b = 1000 1000, то

~a = 1111 0000,

a > 1 = 0000 0111,

a & b = 0000 1000,

a ^ b = 1000 0111,

a | b = 1000 1111.

В языке предусмотрены две нетрадиционные операции инкремента (++) и декремента (--). Они предназначены для увеличения и уменьшения на единицу значения операнда. Операции ++ и -- можно записывать как перед операндом, так и после него. В первом случае (++n или --n) значение операнда (n) изменяется перед его использованием в соответствующем выражении, а во втором (n++ или n--) - после его использования. Рассмотрим две следующие строки программы:

a = b + c++;

a1 = b1 + ++c1;

Предположим, что b = b1 = 2, c = c1 = 4. Тогда после выполнения операций: a = 6, b = 2, c = 5, a1 = 7, b1 = 2, c1 = 5.

Широкое распространение находят также выражения с еще одной нетрадиционной тернарной или условной операцией ?:. В формуле

y = x ? a: b;

y = a, если x не равно нулю (т.е. истинно), и y = b, если х равно нулю (ложно). Следующее выражение

y = (a>b) ? a: b;

позволяет присвоить переменной у значение большей переменной (а или b), т.е. y = max(a, b).

Еще одним отличием языка является то, что выражение вида а = а + 5; можно записать в другой форме: a += 5;. Вместо знака + можно использовать и символы других бинарных операций (см. табл. 2).

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

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

1. Если один из операндов в выражении имеет тип long double, то остальные тоже преобразуются к типу long double.

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

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

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

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

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

7. В противном случае все операнды преобразуются к типу int. При этом тип char преобразуется в int со знаком; тип unsigned char в int, у которого старший байт всегда нулевой; тип signed char в int, у которого в знаковый разряд передается знак из сhar; тип short в int (знаковый или беззнаковый).

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

В языке Си можно явно указать тип любого выражения. Для этого используется операция преобразования ("приведения") типа. Она применяется следующим образом:

(тип) выражение

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

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

int a = 30000;

float b;

........ b = (float) a * 12;

(переменная a целого типа явно преобразована к типу float; если этого не сделать, то результат будет потерян, т.к. a * 12 > 32767).

Указатели и операции с ними

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

Унарная операция & дает адрес объекта, поэтому оператор

у = &х;

присваивает адрес переменной х переменной у. Операцию & нельзя применять к константам и выражениям; конструкции вида &(х+7) или &28 недопустимы.

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

z = *y; присваивает z значение переменной, записанной по адресу у. Если

y = &x;

z = *у; то z = x.

Объекты, состоящие из знака * и адреса (например, *а), необходимо определить. Делается это, например, так:

int *а, *b, *с;

char *d;

Определение вида char *d говорит о том, что значение, записанное по адресу d, имеет тип char.

Указатели могут встречаться и в выражениях. Если у - указатель на целое, т.е. имело место объявление int *у, то *у может появиться там же, где и любая другая переменная, не являющаяся указателем. Таким образом, следующие выражения вполне допустимы:

*у = 7; *x *=5;

(*z)++;

Первое из них заносит число 7 в ячейку памяти по адресу у, второе увеличивает значение по адресу х в пять раз, третье добавляет единицу к содержимому ячейки памяти с адресом z. В последнем случае круглые скобки необходимы, так как операции с одинаковым приоритетом выполняются справа налево. В результате если, например, *z = 5, то (*z)++ приведет к тому, что *z = 6, а *z++ всего лишь изменит сам адрес z (операция ++ выполняется над адресом z, а не над значением *z по этому адресу).

Указатели можно использовать как операнды в арифметических операциях. Если у - указатель, то унарная операция y++ увеличивает его значение; теперь оно является адресом следующего элемента. Указатели и целые числа можно складывать. Конструкция у + n (у - указатель, n - целое число) задает адрес n-гo объекта, на который указывает у. Это справедливо для любых объектов (int, char, float и др.); транслятор будет масштабировать приращение адреса в соответствии с типом, указанным в определении объекта.

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

Операторы цикла

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

for (выражение_1; выражение_2; выражение_3) тело_цикла

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

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

Примеры:

for (i = 1; i , = =, != и другие работают правильно). В то же время нельзя сравнивать ли6о использовать в арифметических операциях указатели на разные массивы (соответствующие выражения не приводят к ошибкам при компиляции, но в большинстве случаев не имеют смысла). Любой адрес можно проверить на равенство или неравенство с константой NULL. Указатели на элементы одного массива можно также вычитать. Тогда результатом будет число элементов массива, расположенных между уменьшаемым и вычитаемым объектами.

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

тип имя_массива[...] ... [...] = {список значений};

Примеры:

int a[5] = {0, 1, 2, 3, 4};

char ch[3] = {'d', 'e', '9'};

int b[2][3] = {1, 2, 3, 4, 5, 6};

В последнем случае: b[0][0] = 1, b[0][1] = 2, b[0][2] = 3, b[1][0] = 4, b[1][1] = 5, b[1][2] = 6.

В языке допускаются массивы указателей, которые определяются, например, следующим образом: char *m[5];. Здесь m[5] - массив, содержащий адреса элементов типа char.

Строки символов

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

Определение char а[10]; указывает компилятору на необходимость резервирования места для максимум 10 символов. Константа а содержит адрес ячейки памяти, в которой помещено значение первого из десяти объектов типа char. Процедуры, связанные с занесением конкретной строки в массив а, копируют ее по одному символу в область памяти, на которую указывает константа а, до тех пор, пока не будет скопирован нулевой символ, оканчивающий строку. Когда выполняется функция типа printf("%s", а), ей передается значение а, т.е. адрес первого символа, на который указывает а. Если первый символ - нулевой, то работа функции printf() заканчивается, а если нет, то она выводит его на экран, прибавляет к адресу единицу и снова начинает проверку на нулевой символ. Такая обработка позволяет снять ограничения на длину строки (конечно, в пределах объявленной размерности): строка может иметь любую длину, но в пределах доступной памяти.

Инициализировать строку при таком способе определения можно следующим образом:

char array[7] = "Строка";

char s[ ] = {'С', 'т', 'р', 'о', 'к', 'а', '\0'};

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

Второй способ определения строки - это использование указателя на символ. Определение char *b; задает переменную b, которая может содержать адрес некоторого объекта. Однако в данном случае компилятор не резервирует место для хранения символов и не инициализирует переменную b конкретным значением. Когда компилятор встречает оператор вида b ="IBM PC";, он производит следующие действия. Во-первых, как и в предыдущем случае, он создает в каком-либо месте объектного модуля строку "IBM PC", за которой следует нулевой символ ('\0'). Во-вторых, он присваивает значение начального адреса этой строки (адрес символа 'I') переменной b. Функция printf("%s", b) работает так же, как и в предыдущем случае, осуществляя вывод символов до тех пор, пока не встретится заключительный нуль.

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

Для ввода и вывода строк символов помимо scanf( ) и printf() могут использоваться функции gets( ) и puts( ) (их прототипы находятся в файле stdio.h).

Если string - массив символов, то ввести строку с клавиатуры можно так:

gets(string);

(ввод оканчивается нажатием клавиши ). Вывести строку на экран можно следующим образом:

puts(string);

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

Наиболее часто используются функции strcpy( ), strcat( ), strlen( ) и strcmp( ).

Если string1 и string2 - массивы символов, то вызов функции strcpy( ) имеет вид:

strcpy(string1, string2);

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

Вызов функции strcat( ) имеет вид:

strcat(string1, string2);

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

Функция strlen( ) возвращает длину строки, при этом завершающий нулевой байт не учитывается. Если a - целое, то вызов функции имеет вид:

a = strlen(string);

Функция strcmp( ) сравнивает две строки и возвращает 0, если они равны.

Структуры

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

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

struct тип { тип элемента_1 имя элемента_1;

.........

тип элемента_n имя элемента_n;

};

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

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

sruct date { int day;

int month;

int year;

};

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

struct date {...} a, b, c;

(при этом выделяется соответствующая память). Описание без последующего списка не выделяет никакой памяти; оно просто задает форму структуры. Введенное имя типа позже можно использовать для объявления структуры, например:

struct date days;

Теперь переменная days имеет тип date.

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

Разрешается вкладывать структуры друг в друга, например:

struct man { char name[20], fam[20];

struct date bd;

int age;

};

Определенный выше тип data включает три элемента: day, month, year, содержащий целые значения (int). Структура man включает элементы name, fam, bd и voz. Первые два - name[20] и fam[20] - это символьные массивы из 20 элементов каждый. Переменная bd представлена составным элементом (вложенной структурой) типа data. Элемент age содержит значения целого типа int). Теперь можно определить переменные, значения которых принадлежат введенному типу:

struct man man_[100];

Здесь определен массив man_, состоящий из 100 структур типа man.

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

man_[j].age = 19;

man_[j].bd.day = 24;

man_[j].bd.month = 2

man_[j].bd.year = 1987;

При работе со структурами необходимо помнить, что тип элемента определяется соответствующей строкой описания в фигурных скобках. Например, массив man_ имеет тип man, year является целым числом и т.п. Поскольку каждый элемент структуры относится к определенному типу, его имя может появиться везде, где разрешено использование значений этого типа. Допускаются конструкции вида man_[i]=man_[j]; где man_[i] и man_[j] - объекты, соответствующие единому описанию структуры. Другими словами, разрешается присваивать одну структуру другой по их именам.

Унарная операция & позволяет взять адрес структуры. Предположим, что определена переменная day:

struct date {int d, m, у;} day;

Здесь day - это структура типа date, включающая три элемента: d, m, у. Другое определение

struct date *db;

устанавливает тот факт, что db - это указатель на структуру типа date.

Запишем выражение:

db = &day;

В этом случае для выбора элементов d, m, у структуры необходимо использовать конструкции:

(*db).d; (*db).m; (*db).y;

Действительно, db - это адрес структуры, *db - сама структура. Круглые скобки здесь необходимы, так как точка имеет более высокий, чем звездочка, приоритет. Для аналогичных целей в языке Си предусмотрена специальная операция ->. Эта операция выбирает элемент структуры и позволяет представить рассмотренные выше конструкции в более простом виде:

db -> d; db -> m; db -> у;

Оператор typedef

Рассмотрим описание структуры:

struct data {int d, m, у;};

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

struct data а, b, с;

В язык Си введено специальное средство, позволяющее назначать имена типам данных (переименовывать). Таким средством является оператор typedef. Он записывается в следующем виде:

typedef тип имя;

Здесь "тип" - любой разрешенный тип данных и "имя" - любой разрешенный идентификатор.

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

typedef int INTEGER;

После этого можно сделать объявление:

INTEGER а, b;

Оно будет выполнять то же самое, что и привычное объявление int a,b;. Другими словами, INTEGER можно использовать как синоним ключевого слова int.

Битовые поля

Особую разновидность структур представляют собой битовые поля. Битовое поле - это последовательность соседних битов внутри одного, целого значения. Оно может иметь тип signed int или unsigned int и занимать от 1 до 16 битов. Поля размещаются в машинном слове в направлении от младших к старшим разрядам. Например, структура:

struct prim { int a:2;

unsigned b:3;

int c:5;

int d:1;

unsigned d:5; } i, j;

обеспечивает размещение данных в двух байтах (в одном слове). Если бы последнее поле было задано так: unsigned d:6, то оно размещалось бы не в первом слове, а в разрядах 0 - 5 второго слова.

В полях типа signed крайний левый бит является знаковым.

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

РАЗДЕЛ 4. ФУНКЦИИ

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

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

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

Функция объявляется следующим образом:

тип имя_функции(тип имя_параметра_1, тип имя_параметра_2, ...);

Тип функции определяет тип значения, которое возвращает функция. Если тип не указан, то предполагается, что функция возвращает целое значение (int).

При объявлении функции для каждого ее параметра можно указать только его тип (например: тип функция (int, float, ...), а можно дать и его имя (например: тип функция (int а, float b, ...) ).

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

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

тип имя_функции(тип имя_параметра_1, тип имя_параметра_2,...)

{ тело функции

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

return выражение;

Таких операторов в подпрограмме может быть несколько, и тогда они фиксируют соответствующие точки выхода. Например:

int f(int a, int b)

{ if (a > b) { printf("max = %d\n", a); return a; }

printf("max = %d\n", b); return b;

} Вызвать эту функцию можно следующим образом:

c = f(15, 5);

c = f(d, g);

f(d, g);

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

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

void swap(int *a, int *b)

{

int *tmp = *a;

*a = *b;

*b = *tmp;

}

Вызов swap(&b, &c) (здесь подпрограмме передаются адреса переменных b и с) приведет к тому, что значения переменных b и c поменяются местами.

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

Рассмотрим, как функции можно передать массив в виде параметра. Здесь возможны три варианта:

1. Параметр задается как массив (например: int m[100];).

2. Параметр задается как массив без указания его размерности (например: int m[];).

3. Параметр задается как указатель (например: int *m;). Этот вариант используется наиболее часто.

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

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

Препроцессор

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

Директива

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

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

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

#define MAX 25

#define BEGIN {

Первая строка вызывает замену в программе идентификатора MAX на константу 25. Вторая позволяет использовать в тексте вместо открывающей фигурной скобки ( { ) слово BEGIN.

Отметим, что поскольку препроцессор не проверяет совместимость между символическими именами макроопределений и контекстом, в котором они используются, то рекомендуется такого рода идентификаторы определять не директивой #define, а с помощью ключевого слова const с явным указанием типа (это в большей степени относится к Си++):

const int MAX = 25;

(тип int можно не указывать, так как он устанавливается по умолчанию).

Если директива #define имеет вид:

#define идентификатор(идентификатор, ..., идентификатор) подстановка

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

#define READ(val) scanf("%d", &val)

оператор READ(y); воспринимается так же, как scanf("%d",&y);. Здесь val - аргумент и выполнена макроподстановка с аргументом.

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

В макроопределение можно помещать объекты, разделенные знаками ##, например:

#define PR(x, у) x##y

После этого PR(а, 3) вызовет подстановку а3. Или, например, макроопределение

#define z(a, b, c, d) a(b##c##d)

приведет к замене z(sin, x, +, y) на sin(x+y).

Символ #, помещаемый перед макроаргументом, указывает на преобразование его в строку. Например, после директивы

#define PRIM(var) printf(#var"= %d", var)

следующий фрагмент текста программы

year = 2006;

PRIM(year);

преобразуется так:

year = 2006;

printf("year""= %d", year);

Опишем другие директивы препроцессора. Директива #include уже встречалась ранее. Ее можно использовать в двух формах:

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

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

Следующая группа директив позволяет избирательно компилировать части программы. Этот процесс называется условной компиляцией. В эту группу входят директивы #if, #else, #elif, #endif, #ifdef, #ifndef. Основная форма записи директивы #if имеет вид:

#if константное_выражение последовательность_операторов

#endif

Здесь проверяется значение константного выражения. Если оно истинно, то выполняется заданная последовательность операторов, а если ложно, то эта последовательность операторов пропускается.

Действие директивы #else подобно действию команды else в языке Си, например:

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

последовательность_операторов_1

#else последовательность_операторов_2

#endif Здесь если константное выражение истинно, то выполняется последовательность_операторов_1, а если ложно - последовательность_операторов_2.

Директива #elif означает действие типа "else if". Основная форма ее использования имеет вид:

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

последовательность_операторов

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

последовательность_операторов_1

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

последовательность_операторов_n

#endif Эта форма подобна конструкции языка Си вида: if...else if...else if...

Директива

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

устанавливает определен ли в данный момент указанный идентификатор, т.е. входил ли он в директивы вида #define. Строка вида

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

проверяет является ли неопределенным в данный момент указанный идентификатор. За любой из этих директив может следовать произвольное число строк текста, возможно, содержащих инструкцию #else (#elif использовать нельзя) и заканчивающихся строкой #endif. Если проверяемое условие истинно, то игнорируются все строки между #else и #endif, а если ложно, то строки между проверкой и #else (если слова #else нет, то #endif). Директивы #if и #ifndef могут "вкладываться" одна в другую.

Директива вида

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

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

Рассмотрим примеры. Три следующие директивы:

#ifdef WRITE

#undef WRITE

#endif проверяют определен ли идентификатор WRITE (т.е. была ли команда вида #define WRITE...), и если это так, то имя WRITE начинает считаться неопределенным, т.е. не подлежащим замене.

Директивы

#ifndef WRITE

#define WRITE fprintf

#endif проверяют является ли идентификатор WRITE неопределенным, и если это так, то определятся идентификатор WRITE вместо имени fprintf.

Директива #error записывается в следующей форме:

#error сообщение_об_ошибке

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

Директива #line предназначена для изменения значений переменных _LINE_ и _FILE_, определенных в системе программирования Си. Переменная _LINE_ содержит номер строки программы, выполняемой в текущий момент времени. Идентификатор _FILE_ является указателем на строку с именем компилируемой программы. Директива #line записывается следующим образом:

#line номер "имя_файла"

Здесь номер - это любое положительное целое число, которое будет назначено переменной _LINE_, имя_файла - это необязательный параметр, который переопределяет значение _FILE_.

Директива #pragma позволяет передать компилятору некоторые указания. Например, строка

#pragma inline

говорит о том, что в программе на языке Си имеются строки на языке ассемблера. Например:

asm mov ax, 5

asm { inc dx

sub bl, al

} и т.д.

Рассмотрим некоторые глобальные идентификаторы или макроимена (имена макроопределений). Определены пять таких имен: _LINE_, _FILE_, _DATE_, _TIME_, _STDC_. Два из них (_LINE_ и _FILE_) уже описывались выше. Идентификатор _DATE_ определяет строку, в которой сохраняется дата трансляции исходного файла в объектный код. Идентификатор _TIME_ задает строку, сохраняющую время трансляции исходного файла в объектный код. Макрос _STDC_ имеет значение 1, если используются стандартно - определенные макроимена. В противном случае эта переменная не будет определена.

Показать полностью… https://vk.com/doc111429032_440964212
194 Кб, 13 января 2017 в 18:57 - Россия, Москва, СИЮ, 2017 г., doc
Рекомендуемые документы в приложении