Правила записи программы на языке Си 5 Правила формального описания синтаксиса языка программирования 6
Вид материала | Лекции |
Содержание8.20.Указатели и функции 8.21.Оператор typedef 8.22.Дополнительные описания указателей для IBM PC |
- Правила преобразований из одного типа в другой и правила приведения типов в языке Object, 19.03kb.
- Оформление программы на языке Паскаль. Оператор вывода. Описание переменных. Оператор, 186.34kb.
- Программа наименование дисциплины Латинский язык (1,2 уровни), 154.48kb.
- Экзаменационные вопросы по курсу "Методы программирования", 32.44kb.
- Вопросы по курсу Программирование на языке высокого уровня (яву), 102.97kb.
- Структура программы в языке программирования С++. Обмен данными между функциями (параметры, 37.24kb.
- Программа курса " Азы программирования", 26.19kb.
- Структура программы языка Турбо Паскаль Программа на языке Турбо Паскаль имеет вид, 792.5kb.
- Эволюция языков программирования, 493.92kb.
- Структура программы на языке Turbo Pascal, 26.15kb.
8.20.Указатели и функции
Функции, как и другие объекты программы, располагаются в памяти ЭВМ. Любая область памяти имеет адрес, в том числе и та, в которой находятся функция. Имя функции без круглых скобок за ним представляет собой константный адрес этой области памяти. Таким образом, имея функции со следующими прототипами:
double sin(double x);
double cos(double x);
double tan(double x);
мы можем в программе использовать имена sin, cos и tan, которые будут обозначать адреса этих функций.
Можно описать и указатель на функцию. Например, для функции с аргументом типа double, возвращающей значение типа double, описание такого указателя будет выглядеть следующим образом:
double (*fn)(double x);
Здесь, как и в случае указателя на массив, круглые скобки увеличивают приоритет операции *. Если бы они отсутствовали, то была бы описан не указатель на функцию, а функция, возвращающая значение указателя на double.
После того, как описан указатель на функцию, становятся возможными следующие операции:
fn = sin; /* Настройка указателя на функцию sin */
a = fn(x); /* Вызов функции sin через указатель */
fn = cos; /* Настройка указателя на функцию cos */
b = fn(x); /* Вызов функции cos через указатель */
Можно описать массив указателей на функцию и проинициализировать его:
double (*fnArray[3])(double x) = { sin, cos, tan };
Теперь становится возможным следующий цикл:
for(i=0; i<3; i++)
printf( "F(x) = %lf\n", fnArray[i](x) );
Можно описать функцию возвращающую значение указателя на функцию:
double (*fnFunc(int i)) (double x)
{
switch(i)
{
case 0 : return sin;
case 1 : return cos;
case 2 : return tan;
}
}
Описанная функция имеет параметр типа int и возвращает значение указателя на функцию с аргументом типа double, возвращающую значение типа double.
После описания функции fnFunc становится возможным следующий цикл:
for(i=0; i<3; i++)
printf( "F(x) = %lf\n", fnFunc(i)(x) );
8.21.Оператор typedef
Описания, подобные описаниям предыдущего раздела, достаточно сложны для понимания. Для упрощения описаний сложных типов в языке Си предусмотрен оператор typedef. Его использование иллюстрируется следующим синтаксисом:
БНФ:
typedef описание_одного_имени
Под описанием_одного_имени подразумевается любое, сколь угодно сложное описание данного. Но в этом случае имя будет обозначать не имя данного, а имя нового типа, который соответствует типу данного и может быть использован в качестве имени типа в любых других определениях данных. Рассмотрим пример:
typedef double DArray[100];
...
DArray A, B, C;
Если бы в первом описании отсутствовало бы ключевое слово typedef, то имя DArray представляло бы имя массива из 100 элементов типа double, для которого бы выделялся соответствующий объем памяти. При наличии typedef компилятор будет воспринимать имя DArray как имя нового типа данных, а именно, типа массива из 100 элементов типа double. Очевидно, никакой памяти при этом не выделяется.
Во втором описании используется имя нового типа DArray. Каждое из определяемых имен A, B и C будет считаться массивом из ста элементов типа double, и для каждого из них будет выделен соответствующий объем памяти.
Описания указателей на функции из предыдущего раздела можно существенно упростить, используя оператор typedef:
typedef double (*Fun)(double x); /*Тип указателя*/
Fun fnArray[3] = { sin, cos, tan }; /*Массив функций*/
Fun fnFunc(int i) /* Функция, возвращающая функцию */
{
switch(i)
{
case 0 : return sin;
case 1 : return cos;
case 2 : return tan;
}
}
Совершенно очевидно, что последние описания значительно понятнее.
8.22.Дополнительные описания указателей для IBM PC
Рассмотрим некоторые особенности режимов работы процессоров, используемых в компьютерах IBM PC. При этом следует учитывать, что процессоры фирмы Intel с типом ниже 80386 обеспечивают 16-ти битный режим работы, а процессоры 80386 и выше - как 16-ти, так и 32-битный режимы.
Типичный режим работы процессора - 16-битный, который обеспечивается собственно системой MS DOS, или DOS-сессией эмулируемой 32-битной системой Windows-95, Windows NT или OS/2.
В этом режиме процессор может использовать и двухбайтовые и четырехбайтовые адреса.
При использовании четырехбайтовых адресов процессор в 16-ти битном режиме может адресовать область памяти не более, чем в 1 мегабайт. При этом, адрес состоит из двух частей: так называемой сегментной части, которая находится в старших 2 байтах адреса, и смещения, содержащегося в младших 2 байтах адреса.
Физический адрес памяти компьютера вычисляется процессором по следующей формуле:
Физический_адрес = seg * 16 + offs, где seg - двухбайтовый сегментный адрес, offs - двухбайтовое смещение.
Одно двухбайтовое смещение может адресовать не более, чем 64 килобайта памяти (216), то есть так называемый сегмент. Добавление сегментной части к смещению по вышеприведенной формуле и обеспечивает адресацию 1M памяти. Однако, при такой трактовке адреса различные адреса могут указывать на один и тот же байт памяти. Рассмотрим три адреса 246:330, 256:170 и 266:10. И сегментная часть адреса, и смещение в этих адресах записаны в десятичной системе счисления, через двоеточие. Рассчитаем физические адреса для каждого из этих значений:
246 * 16 + 330 = 4266
256 * 16 + 170 = 4266
266 * 16 + 10 = 4266
Из расчета видно, что разные адреса определяют один и тот же физический адрес памяти. Это и объясняет, почему в некоторых случаях сравнение указателей может происходить некорректно. Для устранения этого противоречия вводится понятие нормализованного адреса, то есть такого адреса, у которого значение смещения не превышает 16. Последний из трех адресов примера - нормализованный.
Если для всех данных и кода программы зафиксировать сегментную часть адреса, то для адресации достаточно 2-х байтового адреса, состоящего из одного смещения. При этом, размер адресуемого пространства не может превысить 64 килобайта.
Для обозначения соответствующих адресов, используются специальные ключевые слова: near - обозначает 2-х байтовый (близкий) адрес, far - 4-х байтовый (дальний) адрес.
Все действия над адресами типа far выполняются так, что их сегментная часть не меняется. Это позволяет несколько ускорить операции с указателями, но накладывает ограничение в 64K на массив, адресуемый указателем. Если массив должен быть больше 64K, то следует использовать указатели типа huge, которые автоматически поддерживают нормализацию адреса и, поэтому, могут адресовать массив больший 64K.
Примеры описания подобных указателей:
int near *pi; /* 2-х байтовый указатель */
char far *name; /* 4-х байтовый без нормализации */
double huge *pA; /* 4-х байтовый с нормализацией */
Использовать вышеприведенные описатели указателей можно только при полной уверенности в своих действия. Значительно более просто использовать различные типы адресов, меняя модели памяти. Рассмотрим 16-разрядные модели памяти IBM PC.
В крошечной (Tiny) модели памяти сегментные части всех адресов указывают на один и тот же сегмент, размером не более 64K, в котором располагается и код программы, и данные, и стек. Все адреса двухбайтовые (near).
В маленькой (Small) модели памяти сегментная часть адреса кода указывает на один сегмент, размером не более 64K, сегментная часть адресов данных указывает на другой сегмент, размером не более 64K, в котором располагаются данные и стек. Все адреса двухбайтовые (near).
В средней (Medium) модели памяти адреса кода 4-х байтовые (far), то есть размер кода может достигать 1M. Сегментная часть адресов данных указывает на сегмент, размером не более 64K, в котором располагаются данные и стек. Адреса данных двухбайтовые (near).
В компактной (Compact) модели памяти адреса кода 2-х байтовые (near), то есть размер кода не может превышать 64K. Адреса данных 4-х байтовые (far), то есть размер данных может достигать 1M. Однако, максимальный размер статических данных и стека не превышает 64K. По умолчанию стек устанавливается значительно меньше, например 4K.
В большой (Large) модели памяти все адреса 4-х байтовые (far), то есть и размер кода, и размер данных может достигать 1M. Однако, как и в предыдущей модели, максимальный размер статических данных и стека не превышает 64K. По умолчанию стек устанавливается размером 4K.
В громадной (Huge) модели памяти все организовано так же как и в большой, но размер статических данных может достигать 1M.
Следует обратить внимание на то, что ни в одной модели памяти (даже в huge) нет указателей на данные типа huge. Поэтому работа с массивами большими 64K требует специального описания указателей.
В 32-битных режимах работы 386 процессоров far-адрес состоит из 2-х байтового селектора сегмента и 4-х байтового смещения в сегменте. При этом размер смещения позволяет адресовать 4-х гигабайтное адресное пространство (232).
Для 32-х разрядных режимов могут существовать все вышеперечисленные модели памяти. Однако на практике чаще всего используется так называемая плоская (flat), безсегментная модель памяти. На самом деле она соответствует модели Small с учетом того, что размер сегмента может достигать 4 гигабайт, а смещение в сегменте имеет размер 4 байта. Можно считать, что во flat модели сегментов нет вообще, размер адреса равен 4-м байтам и соответствует физическому адресу (виртуальному) памяти компьютера.