Для организации условных и безусловных переходов в программе на языке Си используются операторы: if - else, switch и goto. Первый из них записывается следующим образом:
if (проверка_условия) оператор_1; else оператор_2;
Если условие в скобках принимает истинное значение, выполняется оператор_1, если ложное - оператор_2. Если вместо одного необходимо выполнить несколько операторов, то они заключаются в фигурные скобки. В операторе if слово else может отсутствовать.
В операторе if - else непосредственно после ключевых слов if и else должны следовать другие операторы. Если хотя бы один из них является оператором if, его называют вложенным. Согласно принятому в языке Си соглашению слово else всегда относится к ближайшему предшествующему ему if.
Оператор switch позволяет выбрать одну из нескольких альтернатив. Он записывается в следующем формальном виде:
switch (выражение)
{
case константа_1: операторы_1;
break;
case константа_2: операторы_2;
break;
................
default: операторы_default;
}
Здесь вычисляется значение целого выражения в скобках (его иногда называют селектором) и оно сравнивается со всеми константами (константными выражениями). Все константы должны быть различными. При совпадении выполнится соответствующий вариант операторов (один или несколько операторов). Вариант с ключевым словом default реализуется, если ни один другой не подошел (слово default может и отсутствовать). Если default отсутствует, а все результаты сравнения отрицательны, то ни один вариант не выполняется.
Для прекращения последующих проверок после успешного выбора некоторого варианта используется оператор break, обеспечивающий немедленный выход из переключателя switch.
Допускаются вложенные конструкции switch.
|
Рассмотрим правила выполнения безусловного перехода, который можно представить в следующей форме:
goto метка;
Метка - это любой идентификатор, после которого поставлено двоеточие. Оператор goto указывает на то, что выполнение программы необходимо продолжить начиная с оператора, перед которым записана метка. Метку можно поставить перед любым оператором в той функции, где находится соответствующий ей оператор goto. Ее не надо объявлять.
РАЗДЕЛ 3. СТРУКТУРИРОВАННЫЕ ТИПЫДАННЫХ
Массивы
Массив состоит из элементов одного и того же типа. Ко всему массиву целиком можно обращаться по имени. Кроме того, можно выбирать любой элемент массива. Для этого необходимо задать индекс, который указывает на его относительную позицию. Число элементов массива назначается при его определении и в дальнейшем не изменяется. Если массив объявлен, то к любому его элементу можно обратиться следующим образом: указать имя массива и индекс элемента в квадратных скобках. Массивы определяются так же, как и переменные:
int a[100];
char b[20];
float d[50];
В первой строке объявлен массив а из 100 элементов целого типа: а[0], а[1],..., а[99] (индексация всегда начинается с нуля). Во второй строке элементы массива b имеют тип char, а в третьей - float.
Двумерный массив представляется как одномерный, элементами которого так же являются массивы. Например, определение char а[10][20]; задает такой массив. По аналогии можно установить и большее число измерений. Элементы двумерного массива хранятся по строкам, т.е. если проходить по ним в порядке их расположения в памяти, то быстрее всего изменяется самый правый индекс. Например, обращение к девятому элементу пятой строки запишется так: а[5][9].
|
Пусть задан массив:
int a[2][3];
Тогда элементы массива а будут размещаться в памяти следующим образом: a[0][0], a[0][1], a[0][2], a[1][0], a[1][1], a[1][2].
Имя массива - это константа, которая содержит адрес его первого элемента (в данном примере а содержит адрес элемента а[0][0]). Предположим, что a = 1000. Тогда адрес элемента а[0][1] будет равен 1002 (элемент типа int занимает в памяти 2 байта), адрес следующего элемента а[0][2] - 1004 и т.д. Что же произойдет, если выбрать элемент, для которого не выделена память? К сожалению, компилятор не отслеживает данной ситуации. В результате возникнет ошибка и программа будет работать неправильно.
В языке Си существует сильная взаимосвязь между указателями и массивами. Любое действие, которое достигается индексированием массива, можно выполнить и с помощью указателей, причем последний вариант будет работать быстрее.
Определение
int a[5];
задает массив из пяти элементов а[0], a[1], a[2], a[3], a[4]. Если объект *у определен как
int *у;
то оператор у = &a[0]; присваивает переменной у адрес элемента а[0]. Если переменная у указывает на очередной элемент массива а, то y+1 указывает на следующий элемент, причем здесь выполняется соответствующее масштабирование для приращения адреса с учетом длины объекта (для типа int - 2 байта, long - 4 байта, double - 8 байт и т.д.).
Так как само имя массива есть адрес его нулевого элемента, то оператор у = &a[0]; можно записать и в другом виде: у = а. Тогда элемент а[1] можно представить как *(а+1). С другой стороны, если у - указатель на массив a, то следующие две записи: a[i] и *(у+i) - эквивалентны.
|
Между именем массива и соответствующим указателем есть одно важное различие. Указатель - это переменная и у = а; или y++; - допустимые операции. Имя же массива - константа, поэтому конструкции вида a = y; a++; использовать нельзя, так как значение константы постоянно и не может быть изменено.
Переменные с адресами могут образовывать некоторую иерархическую структуру (могут быть многоуровневыми) типа указатель на указатель (т.е. значение указателя является адресом другого указателя), указатель на указатель на указатель и т.д. Если указатели адресуют элементы одного массива, то их можно сравнивать (отношения вида <, >, = =,!= и другие работают правильно). В то же время нельзя сравнивать ли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);
(ввод оканчивается нажатием клавиши <Enter>). Вывести строку на экран можно следующим образом:
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 крайний левый бит является знаковым.
Поля используются для упаковки значений нескольких переменных в одно машинное слово с целью экономии памяти. Они не могут быть массивами и не имеют адресов, поэтому к ним нельзя применять унарную операцию &.
Объединение (union)
Объединение - это некоторая переменная, которая может хранить (в разное время) объекты различного типа и размера. В результате появляется возможность работы в одной и той же области памяти с данными различного вида. Для описания объединения используется ключевое слово union, а соответствующий синтаксис аналогичен структурам.
Пусть задано определение:
union r {int ir; float fr; char cr;} z;
Здесь ir имеет размер 2 байта, fr - 4 байта, cr - 1 байт. Размер переменной z будет равен размеру самого большого из трех приведенных типов (т.е. 4 байтам). В один и тот же момент времени z может иметь значение только одной из переменных ir, fr или cr.
Перечислимый тип данных
Перечислимый тип данных предназначен для описания объектов из некоторого заданного множества. Он задается ключевым словом enum. Рассморим пример:
enum seasons (spring, summer, autumn, winter);
Здесь введен новый тип данных seasons. Теперь можно определить переменные этого типа:
enum seasons а, b, с;
Каждая из них (а, b, c) может принимать одно из четырех значений: spring, summer, autumn и winter. Эти переменные можно было определить сразу при описании типа:
enum seasons (spring, summer, autumn, winter) a, b, с;
Рассмотрим еще один пример:
enum days {mon, tues, wed, thur, fri, sat, sun} my_week;
Имена, занесенные в days (также как и в seasons в предыдущем примере), представляют собой константы целого типа. Первая из них (mon) автоматически устанавливается в нуль, и каждая следующая имеет значение на единицу больше, чем предыдущая (tues=1, wed=2 и т.д.).
Можно присвоить константам определенные значения целого типа (именам, не имеющим их, будут, как и раньше, назначены значения предыдущих констант, увеличенные на единицу). Например:
enum days (man=5, tues=8, wed=10, thur, fri, sat, sun} my_week;
После этого mon=5, tues=8,wed=10, thur=11, fri=12, sat=13, sun=14.
Тип enum можно использовать для задания констант true=1 и false=0, например:
enum t_f (false, true) а, b;
РАЗДЕЛ 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 поменяются местами.
Если же в качестве аргумента функции используется имя массива, то передается только адрес начала массива, а сами элементы не копируются. Функция может изменять элементы массива, сдвигаясь (индексированием) от его начала.
Рассмотрим, как функции можно передать массив в виде параметра. Здесь возможны три варианта:
- Параметр задается как массив (например: int m[100];).
- Параметр задается как массив без указания его размерности (например: int m[];).
- Параметр задается как указатель (например: int *m;). Этот вариант используется наиболее часто.
Независимо от выбранного варианта вызванной функции передается указатель на начало массива. Сами же элементы массива не копируются.
Если некоторые переменные, константы, массивы, структуры объявлены как глобальные, то их не надо включать в список параметров вызванной функции.
Классы памяти
В языке Си различают четыре основных класса памяти: внешнюю (глобальную), автоматическую (локальную), статическую и регистровую память.
Внешние (глобальные) переменные определены вне функций и, следовательно, доступны для любой из них. Они могут быть определены только один раз. Выше уже говорилось, что сами функции всегда глобальные. Язык не позволяет определять одни функции внутри других. Область действия внешней переменной простирается от точки во входном файле, где она объявлена, до конца файла. Если на внешнюю переменную нужно ссылаться до ее определения или она определена в другом входном файле, то в подпрограмме или файле она должна быть объявлена как extern.
Например:
extern int a; /* Объявление a; память под переменную не
резервируется */
Автоматические переменные по отношению к функциям являются внутренними или локальными. Они начинают существовать при входе в функцию и уничтожаются при выходе из нее (для них можно использовать ключевое слово auto). Однако оно практически не используется, так как при отсутствии ключевого слова переменные по умолчанию принадлежат к классу auto.
Статические переменные объявляются с помощью ключевого слова static. Они могут быть внутренними (локальными) или внешними (глобальными). Внутренние статические переменные, как и автоматические, локальны по отношению к отдельной функции. Однако они продолжают существовать, а не возникают и не уничтожаются при каждом ее вызове. Другими словами, они являются собственной постоянной памятью для функции. Внешние статические переменные доступны внутри оставшейся части файла после того, как они в нем объявлены, однако в других файлах они неизвестны. Это, в частности, позволяет скрыть данные одного файла от другого файла.
Регистровые переменные относятся к последнему классу. Ключевое слово register говорит о том, что переменная, о которой идет речь, будет интенсивно использоваться. Если возможно, значения таких переменных помещаются во внутренние регистры микропроцессора, что может привести к более быстрой и короткой программе (разработчики компиляторов фирмы Borland утверждают, что оптимизация компиляторов данной фирмы по использованию регистровых переменных сделана так хорошо, что указание использовать переменную как регистровую может только снизить эффективность создаваемого машинного кода). Для регистровых переменных нельзя взять адрес; они могут быть только автоматическими с допустимыми типами int или char.
Таким образом, можно выделить четыре модификатора класса памяти: extern, auto, static, register. Они используются в следующей общей форме:
модификатор_класса_памяти тип список_переменных;
Выше уже говорилось об инициализации, т.е. о присвоении различным объектам начальных значений. Если явная инициализация отсутствует, гарантируется, что внешние и статические переменные будут иметь значение нуль, а автоматические и регистровые - неопределенное значение.
Указатели на функции
В языке Си сама функция не может быть значением переменной, но можно определить указатель на функцию. С ним уже можно обращаться как с переменной: передавать его другим функциям, помещать в массивы и т.п.
Код функции в персональном компьютере занимает физическую память. В этой памяти есть точка входа, которая используется для того, чтобы войти в функцию и запустить ее на выполнение. Указатель на функцию как раз и адресует эту точку входа. Это уже будет обычная переменная и с ней можно делать все, что можно делать с переменной.
Через указатель можно войти в функцию, т.е. запустить ее на выполнение. Объявление вида:
int (*f)();
говорит о том, что f - это указатель на функцию, возвращающую целое значение. Первая пара скобок необходима, без них int *f(); означало бы, что f - функция, возвращающая указатель на целое значение. После объявления указателя на функцию в программе можно использовать объекты: *f - сама функция; f - указатель на функцию. Для любой функции ее имя (без скобок и аргументов) является указателем на эту функцию.