В данной Главе pассматpиваются фоpма и содеpжание объяв лений в языке С пеpеменных, функций и типов. Объявление в С имеет вид:
[спец-хpан][спец-тип]декл[=иниц][,декл[=иниц]]...
где "спец-хpан" это спецификатоp хpанения; "спец-тип" это имя оп
pеделяемого типа; "иниц" задает значение или последовательность
значений, котоpые будут пpисвоены объявляемой пеpеменной. "декл"
это идентификатоp, котоpый может быть модифициpован пpямоугольными
([]), фигуpными (()) скобками или звездочкой (*).
До своего использования все пеpеменные в С должны быть явно объявлены. В С можно явно объявить функцию ее пpототипом. Если не задать пpототипа, то он будет создан автоматически по той инфоpмации, котоpая пpисутствует пpи пеpвой ссылке на функцию пpи ее опpеделении или вызове.
Язык С имеет стандаpтный набоp типов данных. Пользователь может создавать свои собственные типы данных, объявление котоpых основано на уже имеющихся типах данных. Можно объявлять массивы, стpуктуpы данных и указатели как на пеpеменные, так и на функции.
Объявления тpебуют одного или нескольких "деклаpатоpов". Декла pатоp - это идентификатоp, котоpый может быть модифициpован пpямо угольными ([]), фигуpными (()) скобками или звездочкой (*) для объявления соответственно массива, типа функции или пойнтеpа. Пpи опpеделении пpостых пеpеменных (каковыми являются символьные, целые или с плавающей точкой) или стpуктуp и союзов пpостых пеpеменных, деклаpатоp является пpосто идентификатоpом.
В С опpеделены четыpе спецификатоpа класса хpанения: auto, extern, register и static. Спецификатоp класса хpанения в объявле нии указывает метод хpанения и инициализации объявляемого объекта и какие части пpогpаммы могут его использовать. Расположение объяв лений в исходном тексте пpогpаммы и наличие и отсутствие дpугих объявлений пеpеменной также являются важными фактоpами для опpеделе ния доступности пеpеменных.
Объявления пpототипов функций pассматpиваются в pазделе "Объяв ления функций (пpототипы)" данной Главы и в Главе "Функции" данного Руководства. Инфоpмацию по опpеделениям функций можно найти в Главе "Функции".
В языке С имеются опpеделения для основных типов данных, называ емых "фундаментальными". Их имена пpиведены в Таблице 4.1.
Таблица 4.1. Фундаментальные типы
Интегpальные Типы с плавающей Пpочие
типы точкой
char float void
int double const
short long double volatile
long
signed
unsigned
enum
Необязательные ключевые слова signed и unsigned могут
предшествовать любому интегральному типу, кроме enum, и могут
быть использованы отдельно в качестве спецификаторов типа. При
этом они понимаются соответственно как signed int и unsigned int.
Ключевое слово int, если оно используется отдельно,
воспринимается как signed. Ключевые слова long и short, когда они
используются отдельно, воспринимаются как long int и short int.
Тип long double семантически эквивалентен double, но синтаксически отличается от него.
Ключевое слово void можно использовать в трех случаях: как возвращаемый функцией тип, как список типов аргумента для функции без аргументов, и для модификации указателя
Ключевое слово volatile реализовано синтаксически, но не семантически. Перечислимые типы рассматриваются как фундаментальные.
Примечание
Тип long float более не поддерживается, и его появление в тексте старой программы должно быть заменено на double.
Типы signed char, signed int, signed short int и signed long int (как и соответствующие unsigned и enum) называются "интегральными" типами. Спецификаторы типов float, double и long double называются "плавающими" типами или типами "с плавающей точкой". В объявлении переменной или функции можно использовать любой спецификатор интегрального типа или типа с плавающей точкой.
Можно использовать тип void для объявления функции, которая не возвращает значения, или для объявления указателя на незаданный тип. Когда ключевое слово void появляется отдельно в скобках за именем функции, оно не интерпретируется как спецификатор типа. В данном контексте оно указывает только на то, что у функции нет аргументов. Типы функций рассмотрены в разделе "Объявления функций (прототипы)".
Спецификатор типа const объявляет не подлежащий модификации объект. Ключевое слово const может быть модификатором для любого фундаментального или составного типа, или модифицировать указатель на объект любого типа. Спецификатор типа const может модифицировать typedef. Если объявление составного типа содержит модификатор const, то каждый элемент составного типа считается неизменным. Если элемент объявлен только со спецификатором типа const, то считается что он имеет тип const int. Объекты const можно поместить в раздел памяти, предназначенный только для чтения.
Спецификатор типа volatile объявляет элемент, значение которого может быть изменено без контроля со стороны программы, в которой он появляется. Ключевое слово volatile может быть использовано в тех же обстоятельствах, что и const (которое только что было рассмотрено). Элемент может быть одновременно и const и volatile, в этом случае элемент не может быть изменен в программе, но может быть изменен некоторым асинхронным процессом. Ключевое слово volatile реализовано синтаксически, но не семантически.
С помощью объявлений typedef можно создать дополнительные спецификаторы типов (см. раздел "Объявления типов"). При использовании таких спецификаторов в объявлении их можно изменить только модификаторами const и volatile.
Спецификаторы типа обычно записываются сокращенно, как это показано в Таблице 4.2. Интегральные типы по умолчанию имеют знак. Т.о. если в спецификаторе типа не используется ключевое слово unsigned, то интегральный тип имеет знак, даже если не указано ключевое слово signed.
В некоторых реализациях можно задать опцию компилятора, по которой тип char сменит установку по умолчанию с signed на unsigned. При использовании этой опции запись char означает unsigned char, и нужно использовать ключевое слово signed для объявления символьного значения со знаком. Опции компилятора описаны в Вашем Руководстве по компилятору.
Примечание
В данном Руководстве, как правило, используется сокращенная форма написания спецификаторов типа, приведенная в Таблице 4.2, а не полная форма, и предполагается, что тип char по умолчанию имеет знак. Т.о. вместо signed char стоит просто char.
Таблица 4.2. Спецификаторы типов и их сокращения
Спецификатор типа Сокращенное написание
signed char char
signed int signed, int
signed short int short, signed short
signed long int long, signed long
unsigned char -
unsigned int unsigned
unsigned short int unsigned short
unsigned long int unsigned long
float -
const int const
volatile int volatile
const volatile int const volatile
Когда вы задаете, что тип char по умолчанию не имеет знака
(указывая соответствующую опцию компилятора), нельзя использовать
сокращенный тип записи для signed char.
Когда вы задаете, что тип char по умолчанию не имеет знака (указывая соответствующую опцию компилятора), можно использовать сокращенный тип записи для unsigned char в виде char.
В Таблице 4.3 приводятся размеры памяти, которые выделяются для хранения каждого фундаментального типа, и диапазоны значений, которые могут храниться в переменной каждого типа. Спецификатор void не включен в таблицу, т.к. он используется только для указания на функцию без возвращаемого значения и для указателя на незаданный тип. Аналогично, таблица не содержит const и volatile, т.к. размер переменных не модифицируется ими и переменные могут содержать любое значение из диапазона своего фундаментального типа.
Таблица 4.3. Память и диапазон значений для фундаментальных
типов
Тип Память Диапазон значений (Внутренний)
char 1 байт от -128 до 127
int зависит от
реализации
short 2 байта от -32,768 до 32,767
long 4 байта от -2,147,483,648 до 2,147,483,647
unsigned char 1 байт от 0 до 255
unsigned зависит от
реализации
unsigned short 2 байта от 0 до 65,535
unsigned long 4 байта от 0 до 4,295,967,295
float 4 байта запись по стандарту IEEE,
будет рассмотрена далее
double 8 байт запись по стандарту IEEE,
будет рассмотрена далее
long double 8 байт запись по стандарту IEEE,
будет рассмотрена далее
Тип char хранит целые значения элементов представительного
набора символов. Это целое значение есть код ASCII для
соответствующего символа. Из-за того, что тип char
интерпретируется как целое со знаком, занимающее 1 байт,
переменная char может хранить значения от -128 до 127, хотя
символьный эквивалент имеют только значения от 0 до 127.
Аналогично, unsigned char может хранить значения в диапазоне от 0
до 255.
Обратите внимание на то, что язык С не определяет размер памяти и диапазон значений для типов int и unsigned int. Вместо этого используется стандартный размер целого в конкретной машине. Например, в 16-битовой машине тип int обычно занимает 16 бит, или 2 байта. В 32-битовой машине тип int обычно занимает 32 бита, или 4 байта. Т.о. тип int эквивалентен либо типу short int либо long int, а тип unsigned int эквивалентен либо типу unsigned short либо типу unsigned long, в зависимости от реализации.
Спецификаторы типа int и unsigned int (или просто unsigned) задают некоторые характеристики языка С (например, тип enum, который будет рассмотрен позднее в разделе "Объявления типов"). В данных случаях определения int и unsigned int для конкретной реализации определяют действительный размер выделяемой памяти.
Примечание
Спецификаторы типов int и unsigned int широко используются в программах на языке С, т.к. они позволяют конкретной машине обрабатывать целые величины наиболее эффективным именно для данной машины способом. Однако, из-за того, что размеры типов int и unsigned int разные, программы, которые зависят от конкретных размеров int могут не переноситься на другие ЭВМ. Чтобы увеличить мобильность программ, можно вместо жесткого кода размеров данных использовать использовать выражения с оператором sizeof. Реальный размер int и unsigned int следует уточнить по Вашему Руководству по компилятоpу.
Числа с плавающей точкой используют фоpмат IEEE(Институт Ин женеpов по Электpике и Электpонике, Инк.). Значение типа float занимают 4 байта, котоpые содеpжат бит знака, 8 бит двоичной экспоненты и 23 бита мантиссы. Мантисса пpедставляет собой число от 1.0 до 2.0. Т.к. стаpший бит мантиссы всегда pавен 1, то он не хpанится в числе. Пpи таком пpедставлении в типе float можно хpа нить величины в диапазоне пpиблизительно от 3.4E-38 до 3.4.E+38.
Значения типа double занимают 8 байт. Формат аналогичен формату float, за тем исключением, что на экспоненту выделяется 11 бит, а на мантиссу 52 бита плюс подразумеваемый старший бит 1. С помощью этого формата для типа double можно представлять величины в диапазоне приблизительно от 1.7Е-308 до 1.7Е+308.
Диапазон значений переменной ограничивается минимальным и максимальным значениями, которые могут быть внутренне представлены заданным числом бит. Однако, в соответствии с правилами преобразования в С (которые подробно излагаются в Главе "Выражения и присвоения") нельзя всегда использовать максимальное и минимальное значение для константы конкретного типа в выражении.
Например, постоянное выражение -32768 состоит из оператора арифметического отрицания (-), примененного к постоянной величине 32,768. 32,768 слишком велико, чтобы быть представлено short int, поэтому ему присваивается тип long. Соответственно, постоянное выражение -32768 имеет тип long. Можно представить -32,768 как short int только приведением типа short. При приведении типа не происходит потери информации, т.к. -32,768 будет иметь внутреннее представление из 2 байт.
Аналогично, значение 65,000 может быть представлено unsigned short приведением типа или заданием значения в восьмеричной или шестнадцатеричной форме записи. В десятичной форме записи считается, что 65,000 это константа со знаком. Значению присваивается тип long, т.к. 65,000 не умещается в short. Можно привести значение long к типу unsigned short без потери информации, т.к. 65,000 может поместиться в 2 байта, если оно хранится как число без знака.
Восьмеричные и шестнадцатеричные константы могут иметь тип signed или unsigned в зависимости от их размера (см. раздел "Целые константы"). Однако, при присваивании типа восьмеричным и шестнадцатеричным константам используется метод, при котором они всегда при преобразовании типа ведут себя как целые без знака.
Среди типов данных языка С можно выделить две категории: скаляры и составные. Скалярные типы включают указатели и арифметические типы. Арифметические типы включают типы с плавающей точкой и целые, как это описано в данном разделе. Составные типы включают массивы и структуры. В Таблице 4.4 показаны категории типов данных С.
Таблица 4.4. Категории типов данных С
Тип категории Тип данных
Интегральные char
int
short
long
signed
Арифметические unsigned
enum
Скалярные enum
С плавающей точкой float
double
long double
Составные указатели
массивы
структуры
Синтаксис: идентификатор
декларатор[[постоянное-выражение]]
*декларатор
(декларатор)
Язык С позволяет объявлять "массивы" значений, "указатели"
на значения, "возвращаемые функцией" значения заданного
типа. Для объявления этих элементов нужно использовать
"декларатор".
"Декларатор" это идентификатор, который может модифицироваться прямоугольными ([]) или круглыми (()) скобками и звездочкой (*) для объявления соответственно массива, типа функции или указателя. Деклараторы появляются в описанных в данной Главе объявлениях массивов, указателей и функций. В следующем разделе приводятся правила формирования и интерпретации деклараторов.
Если декларатор состоит из немодифицированного идентификатора, то объявляемый элемент имеет базовый тип. Если за идентификатором следуют прямоугольные скобки ([]), то это объявление массива. Если слева от идентификатора стоит звездочка (*), то это объявление указателя. Если за идентификатором следуют круглые скобки, то это возвращаемое функцией значение.
Для того, чтобы декларатор был полным он должен обязательно включать спецификатор типа. Спецификатор типа задает тип элементов массива, тип объектов, на которые указывает указатель, или возвращаемый функцией тип.
Детальное описание каждого типа объявлений массива, указателя и функции подробно рассматpивается в последующих pазделах данной Главы.
Следующие пpимеpы иллюстpиpуют пpостейшие фоpмы деклаpатоpов:
Пpимеp 1
Объявление массива целых значений с именем list:
int list[20];
Пpимеp 2
Объявление пойнтеpа с именем сp на значение char:
char *cp;
Пpимеp 3
Объявление функции с именем func без аpгументов с возвpащаемым значением double:
double func(void);
Можно заключить любые деклаpатоpы в скобки для задания конкpетной pеализации сложного деклаpатоpа.
"Сложный" деклаpатоp это идентификатоp, котоpый задает мо дификатоp для нескольких массивов, или функций. Можно использовать pазные комбинации модификатоpовмассива, и функции с одним иденти фикатоpом. Однако, в деклаpатоpе недопустимо появление следующих некоppектных комбинаций.
Массив не может иметь функции в качестве своих элементов.
Функция не может возвpащать массив или функцию.
Пpи интеpпpетации сложных деклаpатоpов пpямоугольные и кpуглые скобки (модификатоpы, стоящие спpава от идентификатоpа) имеют пpе имущество над звездочкой (модификатоpом котоpый стоит слева от иден тификатоpа). Скобки имеют pавный пpиоpитет и ассоцииpуются слева напpаво. После полной интеpпpетации деклаpатоpа на последнем шаге пpименяется тип спецификатоpа. Используя кpуглые скобки можно изменить пpинятый по умолчанию поpядок ассоциации и назначить конкpетную интеpпpетацию.
Пpостой способ интеpпpетации сложных деклаpатоpов состоит в том, что они читаются изнутpи наpужу по следующим четыpем этапам:
В следующем пpимеpе этапы помечены так, в какой последова тельности они интеpпpетиpовались:
char *(*(*var)())[10];
^ ^ ^ ^ ^ ^ ^
7 6 4 2 1 3 5
Пpимеpы 2 - 9 являются дополнительной иллюстpацией объявлений
и показывают, как скобки могут воздействовать на смысл объявления.
Пpимеp 2
В следующем пpимеpе модификатоp массива имеет пpиоpитет над модификатоpом, поэтому var это массив. Модификатоp пpименяется к типу элементов массива. Т.о. элементы массива это значения int.
/* массив значения int */
int *var[5];
Пpимеp 3
В следующем пpимеpе скобки дают модификатоpу пpиоpитет над модификатоpом массива, и var это массив пяти значений int.
/*массив значений int */
int (*var) [5]
Пpимеp 4
Модификатоpы функций имеют пpиоpитет над модификатоpами, поэтому var это функция, возвpащающая значение long.
/*функция, возвpащающая значение long */ long *var(long,long);
Пpимеp 5
Данный пpимеp аналогичен Пpимеpу 3. Скобки дают модификатоpу пpиоpитет над модификатоpом функции и var будет функция, котоpая возвpащает значение long. Функция снова пpинимает два аpгумента long.
/*функция, возвpащающая значение long*/
long (*var)(long,long);
Пpимеp 6
Элементы массива не могут быть функциями, но в данном пpимеpе показано, как вместо этого объявить массив указателей на функции. D пpимеpе var это массив из пяти указателей на функции, котоpые возвpа щают стpуктуpы с двумя компонентами. Аpгументами для функций объявле ны две стpуктуpы с тем же самым типом стpуктуpы, both. Обpатите вни мание, что необходимо заключить *nar[5] в скобки. Без этих скобок данное объявление будет некоppектной попыткой объявить массив функций, как это показано:
/*некоppектно*/
struct both *var[5](struct both, struct both);
/*массив функции, возвpащающие стpуктуpы*/
struct both {
int a;
char b;
} (*var[5])(struct both, struct bith);
Пример 7
В данном примере показано, как объявить функцию, возвращающую указатель на массив, т.к. недопустимы функции, возвращающие массив. Здесь var это функция, возвращающая указатель на массив из трех значений double. Тип аргумента задается сложным абстрактным декларатором. Требуются скобки вокруг звездочки в типе аргумента. Без них тип аргумента будет массивом из трех указателей на значения double. Обсуждение и примеры абстрактных деклараторов можно найти в разделе "Имена типов".
/* функция, возвращающая указатель
на массив из 3 значений double */
double (*var(double(*)[3]))[3];
Пример 8
Как показано в данном примере указатель может указывать на другой указатель, и массив может содержать в качестве элементов другие массивы. Здесь var это массив из пяти элементов. Каждый элемент это массив из пяти указателей, которые указывают на об'единения,каждый из которых имеет две компоненты.
/* массив массивов указателей, указывающих на об'единения */
union sign {
int x;
unsigned y;
} **var[5][5];
Пример 9
В данном примере показано, как задание скобок изменяет смысл объявления. В примере var это массив из пяти элементов, которые являются указателямии на массивы из пяти элементов-указателей на об'единения.
/* массив указателей на массивы указателей на об'единения */
union sign *(*var[5])[5];
Синтаксис: [хран-спец]тип-спец декларатор[,декларатор]...
В данном разделе описана форма и значение объявления
переменной. Объясняется, как объявлять:
Простые Отдельные переменные с одним значением
переменные интегрального типа или с плавающей точкой
Перечислимые Простые переменные интегрального типа, которые
переменные хранят одно значение из набора поименованных
целых констант
Структуры Переменные, составленные из набора значений,
которые могут иметь разный тип
Об'единения Переменные, составленные из нескольких значений
разного типа, которые занимают одну область памяти
Массивы Переменные, составленные из набора значений одного
типа
Пойнтеры Переменные, которые указывают на другие переменные
и содержат местоположение переменной (в форме
адреса) вместо ее значения
В общей форме объявления переменной "тип-спец" задает тип
данных переменной, а "декларатор" задает имя переменной, возможно
модифицированное для объявления типа массива или пойнтеpа. "Тип
-спец" может быть составным, когда тип модифициpует const,
volatile или одиним из специальных ключевых слов, описанных в
pазделе "Объявления со специальными ключевыми словами". Можно
объявить сpазу несколько пеpеменных, если в объявлении исполь
зовать чеpез запятую множественные деклаpатоpы. Hапpимеp, int
const far *fp объявляет пеpеменную с именем fr как far на неизме
няемое значение int.
"Хpан-спец" задает класс хpанения пеpеменной. В некотоpых случаях можно инициализиpовать пеpеменную в момент ее объявления. Инфоpмация, касающая классов хpанения и инициализации, содеpжится соответственно в pазделах "Kлассы хpанения" и "Инициализация".
Синтансис:
[хpан-спец]тип-спец идентификатоp[.идентификатоp]...
В объявлении пpостой пеpеменной задается ее имя и тип. Kpоме
того, можно задать класс хpанения пеpеменной, как это описано в pаз
деле "Kлассы хpанения". "Идентификатоp" в объявлении это имя пеpе
менной. "Тип-спец" это имя опpеделяемого типа данных.
Можно в одном объявлении задать несколько пеpеменных, вводя
их идентификатоpы списком чеpез запятую. Kаждый идентификатоp в
списке имен задает имя пеpеменной. Все опpеделяемые в объявлении
пеpеменные имеют один тип.
Пpимеp 1
int x;
int const y=1;
Пример 2
В этом примере объявляются две переменные с именами reply и flag. Обе переменные имеют тип unsigned long и содержат целые значения без знака.
unsigned long reply, flag;
Пример 3
В следующем примере объявляется переменная с именем order, которая имеет тип double и может содержать значения с плавающей точкой.
double order;
Синтаксис:
enum[признак]{список}[декларатор[,декларатор]...];
enum признак[идентификатор[,декларатор]...];
В "объявлении перечислимого типа" задается имя перечислимой
переменной и определяется набор поименованных целых констант
("перечислимый набор"). Переменная перечислимого типа хранит одно
из значений перечислимого набора, определенного этим типом. Целые
константы перечислимого набора имеют тип int; т.о. объем памяти
для размещения перечислимой переменной совпадает с тем, который
выделяется отдельному значению int.
Переменные типа enum во всех случаях обрабатываются так, как если бы они имели тип int. Они могут использоваться в выражениях индексов и как операнды во всех арифметических операторах и операторах отношений.
Объявления перечислимого типа начинаются с ключевого слова enum и имеют две формы, которые показаны в начале данного раздела и рассматриваются ниже:
"Список" имеет следующий вид:
идентификатор[=постоянное-выражение]
[,идентификатор[=постоянное-выражение]...]
Каждый "идентификатор" в перечислимом списке есть имя
перечислимого набора. По умолчанию, первый идентификатор
связывается со значением 0, следующий идентификатор связывается
со значением 1, и т.д. до последнего идентификатора в объявлении.
Имя перечислимой константы эквивалентно ее значению.
Необязательное сочетание "=постоянное-выражение" отменяет установленную по умолчанию последовательность значений. Т.о. если "идентификатор=постоянное-выражение" появляется в списке, то указанный идентификатор связывается с заданным выражением. "Постоянное-выражение" должно иметь тип int и может быть отрицательным. Следующий идентификатор списка связывается со значением "постоянное-выражение"+1, если не связать его явно с другим значением.
К компонентам списка применяются следующие правила:
Пример 1
В примере определяется перечислимый тип с именем day и объявляется переменная с именем workday перечислимого типа. Значение 0 связывается с saturday по умолчанию. Идентификатор sunday явно устанавливается на 0. Остальным идентификаторам по умолчанию присваиваются значения от 1 до 5.
enum day {
saturday,
sunday=0,
monday,
tuesday
wednesday,
thursday,
friday
} worrday;
Пример 2
В данном примере значение из определенного в Примере 1 набора присваивается переменной today. Обратите внимание на то, что для присваивания значений используются имена перечислимых констант. Достаточно просто использовать перечислимый признак, т.к. пеpечислимый тип day был опpеделен pанее.
enum day today=wednesday;
Синтаксис:
struct [пpизнак]{список-объявлений-компонент}
[деклаpатоp[,деклаpатоp]...];
struct пpизнак[деклаpатоp[,деклаpатоp]...];
"Объявление стpуктуpы" задает имя стpуктуpной пеpеменной и
последовательность значений пеpеменной (называемых "компонентами"
стpуктуpы), котоpые могут иметь pазличные типы. Пеpеменная этого
типа стpуктуpы содеpжит опpеделенные этим типом последовательности.
Объявления стpуктуp начинаются с ключевого слова struct и имеют две фоpмы:
Стpуктуpный пpизнак должен отличаться от пpизнаков дpугих стpуктуp, пеpечислимых типов той же самой сфеpы действия.
"Список-объявлений-компонент" содеpжит объявления одной или нескольких пеpеменных или битовых полей.
Kаждая объявленная пеpеменная из списка есть компонент стpук туpного типа. Объявления пеpеменных в списке компонент имеют ту же фоpму, что и дpугие объявления пеpеменных, pассмотpенные в этой Главе. Исключение состоит в том, что и дpугие объявления не могут содеpжать спецификатоpы класса хpанения или инициализатоpы. Kомпоненты стpуктуpы могут быть любого допустимого типа: фундаментальные, массивы или стpук туpы.
Kомпонент не может быть объявлен таким обpазом, что он имеет тип стpуктуpы, в котоpой он появляется. Однако, компонент все же может быть объявлен как тип стpуктуpы, в котоpой он появляется, если тип стpуктуpы имеет пpизнак. Это позволяет создавать связанные списки стpуктуp.
Объявление битового поля имеет следующий вид:
спецификатоp-типа[идентификатоp]:постоянное-выpажение;
"Постоянное-выpажение" задает число бит в поле. "Спецификатоp-типа"
имеет тип int (signed или unsigned) и "постоянное-выpажение" должно
быть неотpицательным целым выpажением. Hе допускается задание мас
сивов битовых полей, пойнтеpов на битовые поля и функций, возвpаща
ющих битовые поля. Битовые поля без имени могут использоваться как
"пустые" для целей выpавнивания. Битовое поле без имени с шиpиной 0
гаpантиpует, что для компонента, котоpый следует за ним в списке
объявления компонент, память выделяется начиная с гpаницы int.
Kаждый идентификатоp в списке объявлений компонент должен быть уникальным для этого списка. Однако, он не должен обязательно отличаться от имен дpугих обычных пеpеменных или от идентификатоpов дpугого списка объявлений компонент.
Память
Kомпоненты стpуктуpы хpанятся соответственно поpядку, в котоpом они объявлялись: пеpвый компонент имеет наименьший адpес памяти, а последний компонент - наивысший. Память для каждого компо нента начинается на гpанице памяти, соответствующей его типу. Kpоме того, между компонентами стpуктуpы в памяти могут появляться непоименованные пpобелы.
Битовые поля не хранятся на границах их объявленного типа. Например, битовое поле, объявленное типом unsigned int, упаковывается в оставшееся пространство (если оно есть), если предыдущее битовое поле имело тип unsigned int. В противном случае, оно начинает новый объект на границе int.
Пример 1
В данном примере определяется структурная переменная с именем complex. Эта структура имеет две компоненты типа float, x и y. У типа структуры нет признака, поэтому она не имеет имени.
struct {
float x,y;
} complex;
Пример 2
В этом примере определяется структурная переменная с именем temp. Структура имеет три компоненты: name, id и class. Компонент name это массив из 20 элементов, а id и class это простые компоненты соответственно типа int и long. Идентификатор employee это признак структуры.
struct employee {
char name[20];
int id;
long class;
} temp;
Пример 3
В данном примере определяются три структурные переменные: student, faculty и staff. Каждая структура имеет аналогичный список из трех компонент. Компоненты объявляются как структуры типа employee, определенного в Примере 2.
struct employee student, faculty, staff;
Пример 4
В данном примере определяется структурная переменная с именем x. Первые две компоненты структуры это переменная char и указатель на значение float. Третий компонент, next, объявляется как указатель на определяемый тип структуры (sample).
struct sample {
char c;
float *pf;
struct sample *next;
} x;
Пример 5
В этом примере определяется двумерный массив структур с именем screan. Массив содержит 2000 элементов. Каждый элемент это отдельная структура, состоящая из четырех компонентов - битовых полей: icon, color, underline и blink.
struct {
unsigned icon : 8;
unsigned color : 4;
unsigned underline : 1;
unsigned blink : 1;
} screen[25][80];
Синтаксис:
union [признак]{список-объявлений-компонент}
[декларатор[,декларатор.]..];
union признак[декларатор[,декларатор]...];
В "объявлении об'единения" задается имя переменной об'едине
ния и указывается набор значений переменной, называемых "компо
нентами" об'единения, которые могут иметь различные типы. Пере
менная типа union хранит одно из значений, заданных этим типом.
Объявление об'динения имеет ту же форму, что и объявление структуры, за исключением того, что оно начинается с ключевого слова union, а не struct. Правила объявления структур распространяются и на об'единения, за исключением того, что в об'единениях не допускается использование битовых полей в качест ве компонент.
Память
переменной об'единения выделяется такой размер памяти, который необходим для хранения максимального по размеру компонента об'единения. При хранении меньшего компонента переменная об'единения может содержать неиспользуемое пространство памяти. Все компоненты хранятся в одной и той же области памяти и начинаются по одному адресу. Хранимая величина каждый раз перезаписывается при присвоении значения другому компоненту.
Пример 1
В данном примере определяется переменная типа sign и объявляется переменная с именем number, которая имеет две компоненты: целую со знаком svar, и целую без знака uvar. Данное объявление позволяет хранить текущее значение number в виде величины со знаком или без знака. sign это признак, который ассоциируется с данным типом об'единения.
union sign {
int svar;
unsigned uvar;
} number;
Пример 2
В данном примере объявляется переменная jack. Компонентами об'единения являются в порядке своего объявления: указатель на значение char, величина char и массив значений float. Для jack выделяется память, которая требуется для для хранения 20-элементного массива f, т.к. f самый большой компонент об'единения. Тип об'единения не определен, т.к. с ним не связано никакого признака.
union {
char *a, b;
float f[20];
} jack;
Пример 3
В данном примере определяется двумерный массив об'единений с именем screen. Массив состоит из 2000 элементов. Каждый элемент массива это об'единение из двух компонент: window1 и screenval. Компонент window1 это структура с двумя компонентами битовых полей, icon и color. Компонент screenval имеет тип int. В любой заданный момент времени каждый элемент об'единения содержит либо int screenval либо представленную window1 структуру.
union {
struct {
unsigned int icon : 8;
unsigned color : 4;
} window1;
int screenval;
} screen[25][80];
Синтаксис: спецификатор-типа декларатор[пост-выражение];
спецификатор-типа декларатор[];
При объявлении массива задается его имя и тип его элементов.
Кроме того, можно задать число элементов массива. Переменная типа
массив рассматривается как указатель на тип элемента массива, как
это описано в разделе "Идентификаторы".
Объявления массивов имеют две формы, как это показано в начале данного раздела. Отличия в синтаксисе состоят в следующем:
спецификатор-типа декларатор[пост-выражение][пост-выражение]...;
Каждое "пост-выражение" в прямоугольных скобках задает число
элементов в заданном измерении: двумерный массив имеет два
выражения в прямоугольных скобках, трехмерный - три и т.д. При
определении многомерного массива в функции можно опустить первое
постоянное выражение, если массив был проинициализирован,
объявлен в качестве формального параметра или объявлен как ссылка
на массив, который был определен явно в каком-либо другом месте
программы.
Можно определять массивы указателей на самые различные типы объектов, используя составные деклараторы, как это описано в разделе "Составные деклараторы".
Память
Память, которая выделяется для хранения заданного типа массива совпадает по размеру с памятью, которая нужна для хранения всех его элементов. Элементы массива хранятся в непрерывной области памяти по возрастающим адресам от первого до последнего. Для разделения элементов массива в памяти пробел не используется.
Массивы хранятся по строкам. Например, следующий массив состоит из двух строк по три колонки в каждой:
cahr A[2][3];
Сначала записываются три колонки первой строки, затем три колонки
второй строки. Это значит, что последний индекс изменяется более
быстро.
Для указания отдельного элемента массива используется выражение индекса, как это описано в разделе "Выражения индекса".
Пример 1
В данном примере объявляется переменная массива с именем scores для 10 элементов, каждый из которых имеет тип int. Переменная с именем game объявляется как простая переменная типа int.
int scores[10], game;
Пример 2
В данном примере объявляется двумерный массив с именем matrix. Массив имеет 150 элементов, каждый из которых имеет тип float.
float matrix[10][15];
Пример 3
В данном примере объявляется массив структур. В этом массиве 100 элементов; каждый из элементов это структура из двух компонент.
struct {
float x,y;
} complex[100];
Пример 4
В данном примере объявляется тип и имя массива указателей на char. Действительное определение name содержится в каком-либо другом месте программы.
extern char *name[];
Синтаксис:
спецификатор-типа *[модифицирующий-спецификатор]декларатор;
При объявлении указателя указывается имя переменной указателя
и задается тип объекта, на который эта переменная указывает.
Объявленная в качестве указателя переменная хранит адрес памяти.
"Спецификатор-типа" задает тип объекта, который может быть фундаментальным, структурным или об'единения. Переменные указателя могут указывать на функции, массивы и другие указатели. (Информация по объявлению более сложных типов указателей содержится в разделе "Сложные объявления".)
Задав в качестве спецификатора типа void можно на некоторое время отложить спецификацию типа, на который указывает указатель. Такой элемент называют "указателем на void" (void *). Если переменная объявлена как указатель на void, то ее можно использовать для указания на объект любого типа. Однако, для выполнения операций над указателем или над объектом, на который он указывает, должен быть задан тип объекта для каждой операции над ним. Такое преобразование может быть выполнено приведением типов.
"Модифицирующий-спецификатор" может быть в виде const или volatile, либо они оба сразу. Этим задается соответственно, что указатель не будет модифицироваться самой программой (const), или что указатель может быть модифицирован некоторым процессом, который протекает независимо от программы (volatile). (Дополнительную информацию по const и volatile можно найти в разделе "Спецификаторы типа".)
"Декларатор" задает имя переменной и может включать модификатор типа. Например, если декларатор представляет массив, то тип указателя модифицируется на указатель к массиву.
Можно объявить указатель на структуру, об'единение или перечислимый тип до определения структуры, об'единения или перечислимого типа. Однако, определение должно появиться до использования указателя в качестве операнда в выражении. Пойнтер объявляется с использованием признака структуры или об'единения (см. Пример 7 в данном Разделе). Такие объявления допустимы благодаря тому, что компилятору не нужно знать размер структуры или об'единения для выделения памяти для переменной указателя.
Память
Размер памяти, который требуется для адреса и значение адреса зависят от реализации компьютера. Не гарантируется, что указатели на разные типы имеют ту же самую длину.
Пример 1
В данном примере объявляется переменная указатель с именем message. Она указывает на переменную типа char.
char *massage;
Пример 2
В данном примере объявляется массив указателей с именем pointers. Массив состоит из 10 элементов, каждый элемент это указатель на переменную типа int.
int *pointers[10];
Пример 3
В данном примере объявляется переменная указатель с именем pointer. Он указывает на массив из 10 элементов. Каждый элемент этого массива имеет тип int.
int (*pointer)[10];
Пример 4
В данном примере объявляется переменная указатель, x, на постоянную величину. Пойнтер может быть изменен для указания на другое значение int, но само значение, на которое он указывает, не может быть изменено.
int const *x;
Пример 5
Переменная y в Примере 5 объявляется как постоянный указатель на значение int. Значение, на которое он указывает, может быть изменено, но сам указатель должен всегда указывать на то же самое место памяти: адрес fixed_object. Аналогично, z это постоянный указатель, но кроме того, он объявлен для указания на значение int, которое не может быть изменено в программе. Дополнительный спецификатор volatile указывает, что хотя значение const int, на которое указывает z и не может быть изменено в программе, оно может быть изменено некоторым процессом вне программы. Объявлением w задается указываемое значение, которое не будет изменено и сам указатель не будет изменен программой. Однако, некоторый внешний процесс может изменить указатель.
const int some_object = 5 ;
int other_object = 37;
int *const y = &fixed_object;
const volatile *const z = &some_object;
*const volatile w = &some_object;
Пример 6
В данном примере объявляются две переменные указателя, которые указывают на структуру типа list. Это объявление может появиться до определения структуры типа list (см. Пример 7), поскольку определение типа list имеет ту же сферу действия, что и объявление.
struct list *next, *previous;
Пример 7
В данном примере определяется переменная line, которая имеет тип структуры с именем list. Структура типа list имеет три компоненты: первый компонент это указатель на значение char, второй - это значение int, а третий - это указатель на другую структуру list.
struct list {
char *token;
int count;
struct list *next;
} line;
Пример 8
В данном примере объявляется переменная record, которая имеет тип структуры id. Обратите внимание на то, что pname объявлен как указатель на другой тип структуры с именем name. Это объявление может появиться до определения типа name.
struct id {
unsigned int id_no;
struct name *pname;
} record;
Пример 9
В данном примере объявляется переменная указатель p, но в объявлении идентификатору p предшествует void *, что означает, что p может позднее быть использован для указания на любой тип объекта. Адрес значения int присваивается p, но над самим указателем нельзя проводить никакие операции до тех пор, пока он не будет явно конвертирован в тип, на который он указывает. Аналогично, недопустимы косвенные операции над объектами, на которые указывает p, пока p не будет явно не будет конвертирован в конкретный тип. И, наконец, использовано приведение типов для преобразования p в указатель на int, и значение p затем увеличивается.
int i; /* p определяется как указатель на объект,
void *p; тип которого не задан */
p = &1; /* адрес целого i присваивается в p, но сам тип
p еще не задан. Операции, подобные p++ все
еще нельзя выполнить */
(int *)p++; /* увеличение значения p происходит после
преобразования его типа к указателю на int */
Синтаксис: [спецификатор-класса-хранения]
[спецификатор-типа]декларатор
([список-формальных-параметров])
[,список-деклараторов]...;
"Объявление функции", часто называемое "прототипом функции",
задает имя и возвращаемый тип функции, и может задавать типы и
имена формальных параметров и число аргументов функции.
Объявление функции не определяет тела функции. Оно просто
сообщает некоторую информацию о функции компилятору. Это
позволяет компилятору проверить типы действительных аргументов,
задаваемые в вызове функции.
Если не задать прототип функции, то компилятор сконструирует его из первой обнаруженной ссылки на функцию, есть ли это вызов функции или ее определение. этот прототип будет отражать корректные типы параметров только в том случае, когда определение функции присутствует в том же исходном файле. Если определение происходит в другом модуле, то ошибки несовпадения аргументов могут не быть обнаружены. Детальное описание определений функций содержится в разделе "Прототипы функций (Объявления)".
"Спецификатор-класса-хранения" может быть задан либо extern либо static. Спецификаторы класса хранения рассматриваются в разделе "Классы хранения".
"Спецификатор-типа" задает возвращаемый функцией тип, а "декларатор" задает имя функции. Если опустить спецификатор типа в объявлении функции, то предполагается, что функция возвращает значение типа int.
"Список-формальных-параметров" рассматривается в следующем разделе.
Финальный "список-деклараторов" в строке синтаксиса соответствует дополнительным объявлениям, сделанным на той же строке. Это могут быть другие функции, возвращающие значения того же типа, что и первая функция, или объявления некоторых переменных, тип которых совпадает с типом, возвращаемым первой функцией. Каждое такое объявление отделяется от предшествующих и последующих запятой.
"Формальные параметры" описывают действительные аргументы, которые могут быть переданы функции. В объявлении функции объявления параметров задают число и тип действительных аргументов. Кроме того, они могут включать идентификаторы для формальных параметров. Хотя параметры и можно опустить в определении функции, все же рекомендуется их указывать, а в действительном прототипе они вообще обязательны. Увеличение количества информации в объявлении вызывает проведение проверки аргументов функции при ее вызове, которое происходит до обработки компилятором определения функции.
Примечание
Идентификаторы, которые используются для задания имен формальных параметров в объявлении прототипа, носят лишь описательный характер. Сфера их действия прекращается в конце объявления. Т.о. они не обязательно должны совпадать с идентификаторами, которые используются в разделе объявления определения функции. Использование тех же самых имен улучшает восприятие текста программы, но не имеет какого-либо иного значения.
Функции могут возвращать значения любого типа, кроме массивов и функций. Т.о. аргумент "спецификатор-типа" в объявлении функции может задавать любой фундаментальный, структурный или тип об'единения. Можно модифицировать идентификатор функции одной или несколькими звездочками (*) для объявления возвращаемого типа указателя.
Хотя функции и не могут возвращать массивы и функции, они могут возвращать указатели на массивы и функции. Можно объявить функцию, которая возвращает указатель на тип массива или функции, модифицировав идентификатор функции звездочкой (*), прямоугольными ([]) или круглыми (()) скобками. Такой идентификатор функции известен, как "сложный декларатор". Правила формирования и интерпретации сложных деклараторов описаны в разделе "Сложные деклараторы".
Все элементы аргумента "списка-формальных-параметров", которые появляются в скобках за декларатором функции, являются необязательными. Имеются два варианта синтаксиса:
[void]
[register][спецификатор-типа][декларатор[[,...][,...]]]
Если в определении функции опущены формальные параметры, то
в скобках должно быть указано ключевое слово void, которое и
указывает, что никакие аргументы в функцию не передаются. Если в
скобках ничего не указано, то нет никакой информации относительно
того, будут ли переданы аргументы в функцию и не проводится
никакой проверки типов аргументов.
Примечание
Пустые скобки в объявлении функции или ее определении не рекомендуются для нового текста программы. Функции без аргументов должны быть объявлены с ключевым словом void, заменяющим список формальных параметров. Такое использование void интерпретируется по контексту и отличается от использования void в качестве спецификатора типа.
Объявление списка формальных параметров может содержать спецификатор класса хранения register, отдельно или вместе со спецификатором типа и идентификатором. Если register не задан, то устанавливается класс хранения auto. Единственным явно допустимым классом хранения является register. Если в скобках содержится только ключевое слово register, то считается, что представлен формальный параметр типа int без имени, которому выделен класс хранения register.
Если присутствует спецификатор типа, то он может быть именем типа любого фундаментального, структурного или типа об'единения (например, int). "Декларатор" для фундаментального, структурного или типа об'единения просто идентификатор переменной, которая имеет этот тип.
Декларатор для указателя, массива или функции может быть сформирован комбинированием спецификатора типа и соответствующего модификатора с идентификатором. В качестве альтернативы может быть использован "абстрактный декларатор" (декларатор без идентификатора). В разделе "Имена типов" объясняется, как формировать и интерпретировать абстрактные деклараторы.
Может быть объявлен полный, частичный или пустой список формальных параметров. Если список содержит по крайней мере один декларатор, переменное число параметров может быть задано, если закончить список запятой, за которой следуют три точки (,...). Ожидается, что функция имеет по крайней мере столько аргументов, сколько деклараторов или спецификаторов типа предшествует последней запятой.
Допускается использование еще одной специальной конструкции в качестве формального параметра: void* представляет собой указатель на объект незаданного типа. Т.о. в вызове функции указатель может указывать на любой тип объекта после его преобразования (например, приведением типов) к указателю на необходимый тип. Обратите внимание на то, что до проведения операций над указателем или объектом по этому адресу, указатель должен быть явно преобразован. Дополнительная информация по void* содержится в разделе "Объявления указателей".
Задание прототипов необязательно, но крайне желательно. Если оно присутствует, то единственно абсолютно необходимыми элементами являются имя функции, открывающая и закрывающая скобка и точка с запятой. Если не задается возвращаемый тип, как в следующем примере, то предполагается, что функция возвращает тип int:
/*** Абсолютная форма объявления функции ***/
minimal_declaration(); /* может иметь или не иметь
аргументов */
Полный прототип функции это то же самое, что определение
функции, кроме того, что вместо задания собственного тела
функции, он заканчивается точкой с запятой (;), сразу же после
закрывающей скобки.
При объявлении параметров допустима любая комбинация элементов, от полного отсутствия информации (как в приведенном выше примере абсолютной формы) до полного прототипа функции. Если прототип вообще не задается, то прототип будет построен по информации из первой ссылки на функцию, обнаруженной в исходном файле.
Пример 1
В этом примере любая включенная в список формальных параметров информация используется для проверки действительных аргументов, которые появляются в вызове функции, который происходит до обработки компилятором определения функции.
double func(void); /* возвращает значение double, но
* не имеет аргументов
*/
fun (void*); /* принимает указатель на
* незаданный тип, возвращает int
*/
char *true(long, long); /* принимает два значения long и
* возвращает указатель на char
*/
* указатель на char, а возвращает
* int
*/
new (register a, char *); /* принимает int с запросом на
* память register и указатель на
* char, а возвращает int
*/
void go(int *[], char *b); /* принимает массив указателей на
* int, используя абстрактный
* декларатор и указатель на char;
* не возвращает значения
*/
void *tu(double v,...); /* принимает по крайней мере одно
* значение double; могут быть
* заданы и другие аргументы;
* возвращает указатель на
* незаданный тип
*/
Пример 2
Данный пример это прототип функции с именем add, которая принимает два аргумента int, представленные идентификаторами num1 и num2, и возвращает значение int.
int add(int num1, int num2);
Пример 3
В этом примере объявляется функция с именем calc, которая возвращает значение double. Простые скобки указывают на неопределенные аргументы функции.
double calc();
Пример 4
Это пример прототипа функции с именем strfind, которая возвращает указатель на char. Функция принимает по крайней мере один аргумент, объявленный формальным параметром char *ptr, как указатель на значение char. Список формальных параметров имеет только один элемент и заканчивается запятой, за которой следует три точки, что указывает на то, что у функции могут быть дополнительные аргументы.
char *strfind(char *ptr,...);
Пример 5
В этом примере объявляется функция с возвращаемым типом void (нет возвращаемого значения). Ключевое слово void замещает список формальных параметров, следовательно, у этой функции нет аргументов.
void draw(void);
Пример 6
В этом примере объявляется функция sum, которая возвращает указатель на массив из трех значений double. Функция sum принимает в качестве аргументов два значения double.
double (*sum(double, double))[3];
Пример 7
В этом примере объявляется функция select, у которой нет аргументов, но которая возвращает указатель на функцию. Возвращаемое значение указателя указывает на функцию, которая принимает один аргумент int, представленный идентификатором number, и возвращает величину int.
int (*select(void))(int number);
Пример 8
В данном примере объявляется функция prt, которая принимает в качестве аргумента указатель на любой тип и возвращает значение int. Эта операция происходит без появления предупреждающего сообщения о несовпадении типов.
int prt(void *);
Пример 9
В данном примере показано объявление массива с именем rainbow из незаданного числа постоянных указателей на функции. Каждая из них принимает по крайней мере один параметр типа int, наряду с незаданным числом других параметров. Каждая из указанных функций возвращает значение long.
long (*const rainbow[]) (int, ...) ;
4.6 Классы хранения
"Классы хранения" переменных определяют, имеет ли данный элемент "локальное" или "глобальное" действие. Переменным с локальным временем действия выделяется новая область памяти каждый раз, когда управление выполнением программы переходит к блоку, в котором они определены. При выходе из этого блока значения переменных теряют смысл.
Элемент с глобальным временем действия существует и имеет значение на всем времени выполнения программы. Все функции имеют глобальное время действия.
Хотя в языке С определены только два типа классов хранения, имеются следующие четыре спецификатора классов хранения:
Таблица 4.5. Спецификаторы классов хранения
Элемент, который определен с Имеет
auto локальное время действия
register локальное время действия
static глобальное время действия
extern глобальное время действия
Четыре спецификатора класса хранения имеют разное предназначение, т.к. они влияют на сферу действия функций и переменных, наряду с их классами хранения. Термин "сфера действия" относится к той части программы, в которой переменная или функция может быть использована по ее имени. Элемент с глобальным временем действия существует на всем протяжении выполнения исходной программы, но он может и не быть доступен из всех частей программы. (Сфера действия и связанная с ней концепция времени действия рассматриваются в Главе "Структура программы".)
Местоположение объявлений функции и переменной в исходном файле также влияет на класс хранения и сферу действия. Объявления вне определений всех функций происходят на "внешнем уровне", а объявление внутри определения функции происходит на "внутреннем уровне".
Точные значения каждого из спецификаторов класса хранения зависят от двух факторов:
При определении переменной на глобальном уровне (т.е. вне всех функций) можно использовать спецификатор класса хранения static или extern, или вообще не задавать спецификатора класса хранения. На глобальном уровне нельзя использовать спецификаторы класса хранения auto и register.
Объявления переменных на глобальном уровне являются либо определениями переменных ("определяющие объявления") либо ссылками на переменные, которые определены в каком-либо ином месте программы ("ссылочные объявления").
Глобальное объявление переменной, в котором осуществляется ее инициализация (явно или неявно) является ее определяющим объявлением. Определение на глобальном уровне имеет несколько форм:
Можно определить переменную на глобальном уровне в исходном файле только один раз. Если задать спецификатор класса хранения static, то можно определить другую переменную под тем же именем со спецификатором класса хранения static в другом исходном файле. Определение static имеет сферой действия только собственный исходный файл, поэтому конфликта не будет.
Спецификатор класса хранения extern объявляет ссылку на переменную, которая определена в каком-либо другом месте. Можно использовать объявление extern для получения доступа к определению, сделанному в другом исходном файле, или для того, чтобы сделать переменную доступной до ее определения в том же исходном файле. После объявления ссылки на переменную на глобальном уровне переменная будет доступна на всей оставшейся части исходного файла, в котором объявлена эта ссылка.
Объявления со спецификатором класса хранения extern не могут содержать инициализаторов, т.к. они ссылаются на переменную, значение которой определено в другом месте.
Для того, чтобы ссылка extern имела смысл, переменная, на которую делается ссылка, должна быть определена только один раз на глобальном уровне. Объявление может быть сделано в любом из исходных файлов, из которых формируется программа.
В сформулированных выше правилах не рассматривается один специальный случай. В объявлении переменной на глобальном уровне можно опустить и спецификатор класса хранения и инициализатор. Например, int n; является корректным глобальным объявлением. Это объявление в зависимости от контекста может иметь два различных значения:
Пример
В данном примере два исходных файла содержат три глобальных объявления i. Только в одном из них есть инициализация. Это объявление, int i=3; , определяет глобальную переменную i с начальным значением 3. externобъявление i в начале первого исходного файла делает глобальную переменную доступной до ее определения в файле. Без объявления extern в функции main нельзя было бы сделать ссылку на глобальную переменную i. Объявление extern переменной i во втором исходном фале также делает эту переменную доступной в этом исходном файле.
Предположим, что функция printf определена где-либо в другом месте программы. Все три функции выполняют одну и ту же задачу: увеличивают значение i печатают его. Будут напечатаны значения 4, 5 и 6.
Если бы переменная i не была инициализирована, то в процессе компоновки ей было бы автоматически присвоено значение 0. Тогда были бы напечатаны значения 1, 2 и 3.
Первый исходный файл:
extern int i; /* ссылка на i,
определенную ниже */
main()
{
i++;
printf("%d\n", i); /* i равно 4 */
next();
}
int i=3; /* определение i */
next()
{
i++;
printf("%d\n", i); /* i равно 5 */
other();
}
Второй исходный файл:
extern int i; /* ссылка на i,
в первом исходном файле */
other()
{
i++;
printf("%d\n", i); /* i равно 6 */
}
Для объявления переменной на локальном уровне можно использовать любой из четырех спецификаторов класса хранения. Если в таком объявлении не задать спецификатор класса хранения, то по умолчанию он будет установлен auto.
Спецификатор класса хранения объявляет переменную с локальным временем действия. Такая переменная доступна только в том блоке, в котором она объявлена. Объявления переменных auto могут содержать инициализаторы, как это описано в разделе "Инициализация". Переменные с классом хранения auto не инициализируются автоматически, поэтому нужно либо явно инициализировать их при объявлении, либо присвоить им значения в операторах блока. Значения неинициализированных переменных auto не определены.
Переменная static auto может быть инициализирована адресом любого глобального или static элемента, но никогда адресом другого элемента auto, т.к. адрес элемента auto не является константой.
Спецификатор класса хранения register дает указание компилятору выделить, если это возможно, регистр для хранения переменной. Хранение данных в регистре ускоряет процесс доступа к ним и сокращает размер программы. Объявленные с классом хранения register переменные имеют ту же сферу действия, что и переменные auto. Число регистров, в которых можно хранить переменные, зависит от конкретного компьютера. Если нет свободных регистров в тот момент, когда компилятор встречает объявление register, то переменной присваивается класс хранения auto и она записывается в память. Компилятор выделяет регистры для хранения переменных в той последовательности, в которой и объявления появляются в исходном файле. В отличие от переменных auto, переменные static продолжают сохранять свои значения и после выхода управления программы из блока. Можно инициализировать переменную static постоянным выражением. Переменная static инициализируется только один раз, в начале выполнения программы, ее повторная инициализация не проводится каждый раз при входе управления программы в блок. Если не инициализировать переменную static явно, то по умолчанию она инициализируется значением 0.
Объявленные со спецификатором класса хранения extern переменные являются ссылками на переменные с теми же именами, которые были объявлены на глобальном уровне в любом из исходных файлов программы. Локальное объявление extern используется для получения доступа из блока к определению переменной на глобальном уровне. Если объявления переменной на глобальном уровне не сделано, то объявленная с ключевым словом extern переменная имеет сферой своего действия только тот блок, в котором она была объявлена.
Пример
В данном примере на глобальном уровне определяется переменная i с начальным значением 1. Объявления extern в функции main используется для объявления ссылки на глобальный уровень i. static переменная a инициализируется значением 0 по умолчанию, т.к. инициализатор отсутствует. При вызове printf (предполагается, что функция prinf определена в некотором другом месте исходной программы) печатаются значения 1, 0, 0 и 0.
В функции other адрес глобальной переменной i используется для инициализации static переменной указателя external_i. Эта схема работает, т.к. глобальная переменная имеет время действия static, что означает неизменность ее адреса. Далее переменная i переопределяется как локальная переменная с начальным значением 16. Это переопределение i не оказывает какого-либо влияния на i глобального уровня, которое скрыто от использования по имени локальной переменной. К значению глобального i можно получить теперь только косвенный доступ из блока через указатель external_i. Попытка присвоить адрес auto переменной i не будет иметь успеха, т.к. он может быть разным при каждом обращении к блоку. Переменная a объявляется как static и инициализируется значением 2. Это a не конфликтует с a в main, т.к. переменные static на локальном уровне доступны только в том блоке, где они объявлены.
Переменная a увеличивается на 2, давая в результате 4. Если в этой программе будет снова вызвана функция other, то начальное значение a будет 4, т.к. локальные переменные static сохраняют свои значения при выходе и повторном входе программы в блок, где они были объявлены.
int i=1;
main()
{
/* ссылка на i, определенную выше */
extern int i;
/* начальное значение 0, а доступно только в main */
static int a;
/* b хранится в регистре, если это возможно */
register int b=0;
/* класс хранения по умолчанию auto */
int c=0;
/* будут напечатаны значения 1, 0, 0, 0 */
printf("%d\n%d\n%d\n%d\n", i, a, b, c);
}
other()
{
/* переменной указателю присваивается адреc
глобального i */
static int *external_i=&i;
/* переопределение i, глобальное i более недоступно */
int i=16;
/* это a доступно только в этой функции */
static int a=2;
a+=2
/* будут напечатаны значения 16, 4 и 1 */
printf("%d\n%d\n%d\n", i, a, *external_i);
}
В объявлениях функций можно использовать спецификатор класса хранения static либо extern. Функции всегда имеют глобальное время действия.
Правила, которые регулируют сферу действия функций очень незначительно отличаются от аналогичных правил для переменных. Они состоят в следующем:
Синтаксис: =инициализатор
Используя инициализатор в объявлении функции можно присвоить
переменной начальное значение. Переменной при этом будет
присвоено значение инициализатора. Инициализатору предшествует
знак равенства (=).
Можно инициализировать переменные любых типов, если придерживаться следующих правил:
Синтаксис: =выражение
Переменной присваивается значение "выражения". Применяются
правила преобразования для присвоений.
Локально объявленная переменная static может быть инициализирована только постоянным выражением. Т.к. адрес любой глобально объявленной или static переменной остается неизменным, его можно использовать для инициализации объявленного static указателя. Однако, адрес переменой auto не может быть использован как инициализатор, т.к. он может быть разным при каждом выполнении блока.
Пример 1
В данном примере x инициализируется постоянным выражением 10.
int x=10;
Пример 2
В данном примере указатель px инициализируется значением 0, давая в результате "пустой" указатель.
register int *px=0;
Пример 3
Данный пример использует постоянное выражение для инициализации c постоянным значением, которое не может быть изменено.
const int c=(3*1024);
Пример 4
В данном примере указатель b инициализируется адресом другой переменной, x. Пойнтер a инициализируется адресом переменной с именем z. Однако, т.к. ожидается, что значение переменной a будет const, ее можно только инициализировать, но не изменять. Она всегда будет указывать на конкретное место памяти.
int *b=&x;
int *const a=&z;
Пример 5
В Примере 5 на глобальном уровне объявляется глобальная переменная GLOBAL, поэтому она будет иметь глобальное время действия. локальная переменная LOCAL имеет класс хранения auto и имеет адрес только при выполнении функции, в которой она объявлена. Попытка инициализации static указателя lp адресом LOCAL не допускается. static указатель gp может быть инициализирован адресом GLOBAL, т.к. он остается неизменным. Аналогично, *rp может быть инициализирован, т.к. rp это локальная переменная и поэтому может иметь непостоянный инициализатор. Каждый раз при входе в блок, LOCAL будет иметь новый адрес, который будет присвоен rp.
int GLOBAL;
int function(void)
{
int LOCAL;
static int *lp=&LOCAL; /* некорректное объявление */
static int *gp=&GLOBAL; /* корректное объявление */
register int *rp=&LOCAL; /* корректное объявление */
Синтаксис: ={список-инициализаторов}
Инициализаторы в "списке-инициализаторов" отделяются друг от
друга запятой. Каждый инициализатор списка это либо постоянное
выражение либо список инициализаторов. Т.о. заключенный в скобки
список инициализаторов может появиться в другом списке
инициализаторов. Эта форма удобна для инициализации составных
компонент составных типов, как это будет показано в данном
разделе.
Значения постоянных выражений списка инициализаторов присваиваются соответствующим компонентам составных переменных. При инициализации об'единения список инициализаторов должен быть простым постоянным выражением. Значение постоянного выражения при этом присваивается первому компоненту об'единения.
Если список инициализаторов содержит меньше значений, чем составной тип, то остальные компоненты или элементы составного типа инициализируются значением 0. Если в списке инициализаторов больше значений, чем в составном типе, то появляется сообщение об ошибке. Эти правила применимы ко всем вложенным спискам инициализаторов, также как и ко всем составным типам в целом.
В следующем примере объявляется массив P размерностью 3 на 4 и элементы его первой строки инициализируются значением 1, второй - 2, третьей - 3 и четвертой - 4:
int P[4][3]={
{1, 1, 1 },
{2, 2, 2 },
{3, 3, 3,},
{4, 4, 4,},
};
Обратите внимание на то, что список инициализаторов для
третьей и четвертой строки содержит запятую после последнего
постоянного выражения. За последним инициализатором ({4, 4, 4,})
также следует запятая. Наличие таких дополнительных запятых
допускается, но они не являются обязательными. Обязательными
являются только запятые, разделяющие постоянные выражения между
собой и один инициализатор от другого.
Если для составного компонента нет встроенного списка инициализаторов, то значения просто присваиваются в порядке следования компонент. Т.о. инициализацию предыдущего примера можно было сделать следующим образом:
int P[4][3]={
1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4
};
Отдельные инициализаторы в списке также могут заключаться в
скобки.
При инициализации составной переменной нужно очень аккуратно использовать скобки и список инициализаторов. Следующий пример более подробно показывает интерпретацию компилятором скобок:
typedef struct {
int n1, n2, n3;
} triplet;
triplet nlist[2][3] = {
{{ 1, 2, 3}, { 4, 5, 6}, { 7, 8, 9}}, /* строка 1 */
{{10,11,12}, {13,14,15}, {16,17,18}}, /* строка 2 */
};
В этом примере объявляется массив структур с именем nlist
размерности 2 на 3, каждая структура при этом имеет три
компоненты. При инициализации первая строка присваивает значения
первой строке nlist следующим образом:
Обратите внимание на то, что требуется внешние скобки, которые заключают в себя инициализаторы строк 1 и 2. Следующая конструкция без внешних скобок вызовет появление ошибки:
triplet nlist[2][3] = {
{ 1, 2, 3}, { 4, 5, 6}, { 7, 8, 9}, /* строка 1 */
{10,11,12}, {13,14,15}, {16,17,18} /* строка 2 */
};
В данной конструкции первая открывающая скобка строки 1
инициализирует nlist[0], который представляет собой массив из
трех структур. Значения 1, 2 и 3 присваиваются трем компонентам
первой структуры. Инициализация nlist[0] заканчивается после
закрывающей скобки (которая стоит после значения 3) и две
оставшиеся структуры массива из трех структур автоматически
инициализируются значением 0. Аналогично, {4,5,6} инициализирует
первую структуру из второй строки nlist. Оставшиеся две структуры
nlist[1] устанавливаются равными 0. Когда компилятор обнаружит
третий список инициализаторов ({7,8,9}), он попытается провести
инициализацию nlist[2]. Но nlist имеет только две строки, поэтому
появится сообщение об ошибке.
Пример 1
В данном примере три int компоненты x инициализируются соответственно значениями 1, 2 и 3. Три элемента первой строки m инициализируются значением 4.0; элементы остальных строк m инициализируются значением 0 по умолчанию.
struct list {
int i,j,k;
float m[2][3];
} x={
1,
2,
3,
{4.0, 4.0, 4.0}
};
Пример 2
В данном примере инициализируется переменная y. Первый элемент об'единения это массив, поэтому инициализатор для него будет составным. Список инициализаторов {'1'} присваивает значения первой строке массива. Т.к. в списке указано только одно значение, то значением 1 будет инициализирован только элемент первой колонки, а остальные два элемента этой строки инициализируются значением ноль по умолчанию. Аналогично, первый элемент второй строки x будет инициализирован значением 4, а оставшиеся два элемента строки инициализируются значением 0.
union
{
char x[2][3];
int i,j,k;
} y = { {
{'1'},
{'4'} }
};
Синтаксис: ="символы"
Можно инициализировать массив символов литеральной строкой.
В следующем примере инициализируется массив из четырех символьных
элементов с именем code. Четвертый элемент это пустой символ,
который ограничивает все стоковые литералы.
char code[ ] = "abc";
Если строка будет длиннее, чем заданный размер массива, то
дополнительные символы будут просто игнорироваться. Например, в
следующем объявлении массив code инициализируется, как символьный
массив из трех элементов:
char code[3] = "abcd";
Только первые три символа инициализатора записываются в
code. Символ d и заканчивающий строку пустой символ
отбрасываются. В результате получается незаконченная строка (т.е.
без значения 0 в конце) и появляется диагностическое сообщение,
указывающее на возникновение этой ситуации.
Если строка короче заданного размера массива, оставшиеся элементы массива инициализируются значениями 0.
В объявлении типов задаются имена компоненты структур или об'единений, или имена и перечислимые наборы для перечислимых типов. Можно использовать имя объявленного типа в объявлениях функций или переменных для ссылки на этот тип. Это удобно в том случае, когда несколько переменных или функций имеют один и тот же тип.
Спецификатор типа задается объявлением typedef. Объявления typedef можно использовать для построения более коротких или более осмысленных имен для уже определенных в С типов или типов, объявленных пользователем.
Объявления структур, об'единений и перечислимых типов имеют ту же общую форму, что и объявления переменных этих типов. Однако есть ряд отличий между объявлениями переменных и типов:
Пример 1
В данном примере объявляется перечислимый тип с именем status. Имя этого типа может быть использовано в объявлениях перечислимых переменных. Идентификатор loss явно устанавливается в -1. bye и tie связываются со значением 0, а win задается значение 1.
enum ststus {
loss = -1,
bye,
tie = 0,
win
};
Пример 2
В данном примере объявляется тип структуры с именем student. Объявление struct student employee; может быть использовано для определения структурной переменной типа student.
struct student {
char name[20];
int id, class;
};
Синтаксис:
typedef спецификатор-типа декларатор [,декларатор]...;
Объявление typedef аналогично объявлению переменной, но
вместо спецификатора класса хранения стоит ключевое слово
typedef. Объявление typedef интерпретируется аналогично
объявлению переменной или функции, но идентификатор становится
синонимом типа, вместо заданного объявлением типа.
Обратите внимание на то, что объявление typedef не создает новый тип. Оно создает синонимы для уже существующих типов или имена для типов, которые могут быть заданы каким-либо другим способом. Когда имя typedef использовано в качестве спецификатора типа, оно может комбинироваться только с определенными спецификаторами типа, но не со всеми. Допустимыми модификаторами являются const и volatile. В некоторых реализациях есть дополнительные специальные ключевые слова, которые могут быть использованы для модификации typedef.
С typedef можно объявить любой тип, включая указатель, функцию и массив. Можно объявить имя typedef для указателя на структуру или об'единение еще до определения структуры или об'единения, если определение имеет ту же сферу действия, что и объявление.
Пример 1
Данный пример объявляет WHOLE синонимом для int. Заметим, что теперь WHOLE можно использовать в объявлении переменной как WHOLE i; или const WHOLE i; . Однако, использование long WHOLE i; недопустимо.
typedef int WHOLE;
Пример 2
В данном примере объявляется тип структуры GROUP с тремя компонентами. Задан признак структуры club, и теперь в объявлениях можно использовать либо имя typedef (GROUP) либо признак структуры.
typedef struct club {
char name[30];
int size, year;
} GROUP;
Пример 3
В данном примере использовано имя typedef предыдущего примера для определения типа указателя. Тип PG объявлен как указатель на тип GROUP, который в свою очередь определен как тип структуры.
typedef GROUP *PG;
Пример 4
В Примере 4 задается тип DRAWF для функции, которая не возвращает значения и принимает два аргумента int. Это значит, например, что объявление DRAWF box; эквивалентно объявлению void box(int, int); .
typedef void DRAWF(int, int);
"Имена типов" задают конкретный тип данных. Кроме обычных объявлений переменных и определенных типов, имена типов можно использовать еще тремя способами: в списке формальных параметров объявления функции, при преобразовании типов и в операциях sizeof. Списки формальных параметров были рассмотрены в разделе "Объявления функций". Преобразования типов и операции sizeof рассмотрены соответственно в Разделах 5.6.2 и 5.3.4 .
Имена типов для фундаментальных и перечислимых типов, структур и об'единений есть просто их спецификаторы типов.
Имя типа для указателя, массива или функции имеет вид:
спецификатор-типа абстрактный-декларатор
"Абстрактный-декларатор" это декларатор без идентификатора, и
состоит обычно из одного или нескольких модификаторов указателя,
массива или функции. Модификатор указателя (*) в деклараторе
всегда предшествует декларатору, модификаторы массива ([]) и
функции (()) следуют за идентификатором. Зная все это можно
определить, где в абстрактном деклараторе должен появиться
идентификатор, и интерпретировать декларатор соответствующим
образом. Дополнительная информация и примеры по сложным
деклараторам содержатся в Разделе 4.3.2 .
Абстрактные деклараторы могут быть сложными. Скобки в сложном абстрактном деклараторе задают его конкретную интерпретацию, как это делается в сложных объявлениях.
Примечание
Абстрактный декларатор, состоящий из пары пустых скобок, (), не допускается, т.к. он не определен. В этом случае невозможно определить, относится ли идентификатор к внутренности скобок (в этом случае это будет немодифицированный тип), либо он действует до скобок (в данном случае это будет тип функции).
Установленный объявлением typedef спецификатор типа кроме того, рассматривается как имя типа.
Пример 1
В данном примере задается имя типа для "указателя на long".
long *
Пример 2
В Примерах 2 и 3 показано, как скобки изменяют значения сложных абстрактных деклараторов. В Примере 2 задается имя типа для указателя на массив из пяти значений int.
int (*) [5]
Пример 3
В Примере 3 задается указатель на функцию, которая не принимает аргументов и возвращает величину int.
int (*) (void)