Функции, как и другие объекты программы, располагаются в памяти ЭВМ. Любая область памяти имеет адрес, в том числе и та, в которой находится функция. Имя функции без круглых скобок за ним представляет собой константный адрес этой области памяти. Таким образом, имея функции со следующими прототипами:
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) );
В языке C понятие массива тесно связано с понятием указателя. Действительно, как было описано выше, имя массива представляет собой адрес области памяти, распределенной под этот массив, или иными словами адрес первого элемента массива. Пусть описаны следующие данные:
int a[100], *pa;
и осуществлено присваивание:
pa = a;
Оно является корректным, поскольку имя a обозначает адрес первого элемента массива a и поэтому имеет тип указателя на int. После этого присваивания
pa[0] или *pa будет обозначать a[0];
pa[1] или *(pa+1) будет обозначать a[1];
pa[2] или *(pa+2) будет обозначать a[2] и т. д. И вообще обозначения вида *(pa+n) и pa[n] являются полностью эквивалентными. Точно также эквивалентны выражения *(a+i) и a[i].
На первый взгляд, кажется, что массив и указатель полностью эквивалентны. Однако имеется два существенных отличия массива от указателя:
· массиву при описании выделяется память для хранения всех его элементов, а указателю только для хранения адреса;
· адрес массива навсегда закреплен за именем, то есть имя массива является адресной константой и выражение вида a = pa недопустимо.
Результат прибавления к указателю или вычитания из него целочисленной величины является указателем того же типа, значение которого отличается от значения исходного указателя на число байт, определяемое произведением целочисленной величины на размер данного, которое адресует указатель. Например, если имеются описания
int A[20], *pA = A;
double B[20], *pB = B;
то указатель (pA+3) будет иметь значение на 6 байт больше, чем pA, и будет адресовать элемент A[3] массива A. Указатель (pB+3) будет иметь значение на 24 байта больше, чем pB, и будет адресовать элемент B[3] массива B. С указателями типа void подобные операции выполнены быть не могут, поскольку компилятор не знает размера адресуемого данного.
Для указателей определены операции увеличения и уменьшения на целочисленную величину, как альтернативная форма записи выражений
pA = pA + i; эквивалентно pA += i;
pA = pA - i; эквивалентно pA -= i;
pA = pA + 1; эквивалентно pA++; или ++pA;
pA = pA - 1; эквивалентно pA--; или --pA; При этом, работа префиксных и постфиксных операций ++ и -- совпадает с их работой для арифметических данных.
Указатели допускается использовать в операциях сравнения. При этом всегда возможно сравнение указателя с нулем и сравнение двух однотипных указателей. Однако правильность результата последнего сравнения для 16-ти разрядного режима работы IBM PC гарантируется только в том случае, если сравниваемые указатели являются указателями на элементы одного и того же массива данных или если они предварительно подвергаются нормализации (см. ниже).
Разность двух однотипных указателей представляет собой целочисленную величину равную количеству элементов данных (не байт) между соответствующими адресами памяти. Правильность результата этой операции для 16-ти разрядного режима работы IBM PC тоже гарантируется только в том случае, если указатели имеют значение адресов элементов одного и того же массива данных или если они предварительно подвергаются нормализации (см. ниже).
В следующем фрагменте программы иллюстрируется использование вышеописанных операций
double A[100], *pA, *pA100;
int i;
/* Заполняем массив A. Работаем с массивом */
for (i=0; i<100; i++) A[i]=0;
/* Заполняем массив A. Работаем с указателями */
for (pA=A, pA100=pA+100; pA<pA100; pA++) *pA=11.9;
Последний вариант заполнения массива может оказаться более эффективным.
Указатели и двумерные массивы
Пусть имеются следующие определения массивов и указателей:
int A[4][2], B[2];
int *p, (*pA)[4][2], (*pAstr)[2];
Здесь A представляет собой двумерный массив из четырех строк и двух столбцов, B - одномерный массив из двух элементов. Для каждого из этих массивов будет выделено соответствующее количество памяти, достаточное для хранения всех их элементов.
Указатель p представляет собой указатель на величину int, указатель pA - указатель на двумерный массив из четырех строк и двух столбцов, pAstr - указатель на одномерный массив из двух элементов. Все указатели имеют размер, равный размеру адреса для данных в используемой модели памяти. Память для хранения данных, естественно, не выделяется. Количество элементов данных из описания массивов будет использовано лишь для корректного изменения значения указателя при выполнении над ним допустимых арифметических операций.
Смысл трактовки этих указателей определяется направлением слева-направо для подряд следующих операций [], а также изменением приоритета операции * с помощью круглых скобок. Если не поставить круглых скобок, то следующее определение
int *pa[4][2];
рассматривается как определение двумерного массива из указателей на тип int.
Для вышеописанных указателей допустимы следующие операции присваивания, поскольку слева и справа от операции присваивания находятся указатели на один и тот же тип данных:
p = B;
p = &B[1];
p = &A[0][0];
p = A[2];
Следующее присваивание:
p = A; /* неверно */
является неверным, так как слева от операции присваивания находится указатель на тип int, а справа - указатель на первый элемент массива A, который (элемент) представляет собой массив из двух элементов типа int. В таких случаях компиляторы выдают предупреждающее сообщение о подозрительном преобразовании указателя.
Если программист уверен в своих действия, то он может использовать операцию явного приведения типа для устранения этого сообщения, но при этом компилятор снимает с себя всякую ответственность за корректность использования такого указателя. Так, после присваивания
p = (int *) A;
элементы, на которые ссылается указатель, и элементы массива A находятся в следующем соответствии:
p[0] эквивалентно A[0][0]
p[1] эквивалентно A[0][1]
p[2] эквивалентно A[1][0]
p[3] эквивалентно A[1][1]
p[4] эквивалентно A[2][0]
p[5] эквивалентно A[2][1]
p[6] эквивалентно A[3][0]
p[7] эквивалентно A[3][1]
Совершенно корректными являются следующие присваивания
pAstr = A;
после которого использование массива A и указателя pAstr совершенно эквивалентны:
pAstr[i][j] эквивалентно A[i][j]
Присваивание
pAstr = &A[2];
устанавливает следующее соответствие между элементами, на которые ссылается указатель pAstr и элементами массива A:
pAstr[0][0] эквивалентно A[2][0]
pAstr[0][1] эквивалентно A[2][1]
pAstr[1][0] эквивалентно A[3][0]
pAstr[1][1] эквивалентно A[3][1]
Следующие присваивания корректны
pA = &A; /* Указатель на двумерный массив */
pAstr = &B; /* Указатель на одномерный массив */
и устанавливают следующее соответствие элементов:
(*pA)[i][j] эквивалентно A[i][j]
(*pAstr)[i] эквивалентно B[i]
Массивы указателей удобны для хранения символьных строк:
char *str[] = {
"Строка 1",
"Строка 2",
"Длинная строка 3"
};
В этом случае каждый элемент массива представляет собой адрес соответствующей строки символов, а сами строки располагаются компилятором в статическом сегменте данных. Никакой лишней памяти, связанной с различной длиной строк, при этом не расходуется.
Пример
В примере обрабатываются элементы одномерного массива. В функции main() элементам массива присваиваются значения их индекса. Промежуточная функция p2() вызывает функцию p1() или p3(), переданную как параметр. Функция p1() выводит элементы массива по порядку, функция p3() – в обратном порядке.