Указатели и двумерные массивы Пусть имеются следующие определения массивов и указателей: 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" }; В этом случае каждый элемент массива представляет собой адрес соответствующей строки символов, а сами строки располагаются компилятором в статическом сегменте данных. Никакой лишней памяти, связанной с различной длиной строк, при этом не расходуется. Указатели и функции Функции, как и другие объекты программы, располагаются в памяти ЭВМ. Любая область памяти имеет адрес, в том числе и та, в которой находятся функция. Имя функции без круглых скобок за ним представляет собой константный адрес этой области памяти. Таким образом, имея функции со следующими прототипами: 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) ); Оператор 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; } } Совершенно очевидно, что последние описания значительно понятнее. |