Указатель (пойнтер, англ. pointer) - это переменная, содержащая адрес другой переменной. Тип данных pointer равен 4 байта. Указатели очень широко используются в языке C. Это происходит отчасти потому, что иногда они дают единственную возможность выразить нужное действие, а отчасти потому, что они обычно ведут к более компактным и эффективным программам, чем те, которые могут быть получены другими способами.
Так как указатель содержит адрес объекта, это дает возможность "косвенного" доступа к этому объекту через указатель.
int * pi = NULL;
pi = &i; cout<<&i<<endl;//записи эквивалентны cout<<pi<<endl;//записи эквивалентны
*pi=48;
#include <iostream> using namespace std; /* возвращает длину строки s */ int strlen(char *s) { int n; for (n = 0; *s != '\0'; s++) n++; return(n); } int main() { int i; int *pi = NULL; pi = &i; cout<<&i<<endl; cout<<pi<<endl; *pi=48; //разыменование - изменение значения переменной на который указывает указатель. cout<<i<<endl; cout<<strlen("ER345")<<endl; }
В языке C существует сильная взаимосвязь между указателями и массивами , настолько сильная, что указатели и массивы действительно следует рассматривать одновременно. Любую операцию, которую можно выполнить с помощью индексов массива, можно сделать и с помощью указателей. Вариант с указателями обычно оказывается более быстрым, но и несколько более трудным для непосредственного понимания. Описание
int a[10];
определяет массив размера 10, т.е. набор из 10 последовательных объектов, называемых a[0], a[1], …, a[9]. Запись a[i] соответствует элементу массива через i позиций от начала. Если pa - указатель целого, описанный как
int *pa;
то присваивание
pa = &a[0];
приводит к тому, что pa указывает на нулевой элемент массива a. Это означает, что pa содержит адрес элемента a[0]. Теперь присваивание
x = *pa
будет копировать содержимое a[0] в x.
Если ра указывает на некоторый определенный элемент массива a, то по определению pa+1 указывает на следующий элемент, и вообще pa-i указывает на элемент, стоящий на i позиций до элемента, указываемого pa, а pa+i на элемент, стоящий на i позиций после. Таким образом, если pa указывает на a[0], то *(pa+1)
ссылается на содержимое a[1], pa+i - адрес a[i], а *(pa+i) - содержимое a[i].
Эти замечания справедливы независимо от типа переменных в массиве a. Суть определения "добавления 1 к указателю", а также его распространения на всю арифметику указателей, состоит в том, что приращение масштабируется размером памяти, занимаемой объектом, на который указывает указатель. Таким образом, i в pa+i перед прибавлением умножается на размер объектов, на которые указывает pa.
Очевидно существует очень тесное соответствие между индексацией и арифметикой указателей. В действительности компилятор преобразует ссылку на массив в указатель на начало массива. В результате этого имя массива является указательным выражением. Отсюда вытекает несколько весьма полезных следствий. Так как имя массива является синонимом местоположения его нулевого элемента, то присваивание pa = &a[0]
можно записать как pa = a.
Еще более удивительным, по крайней мере на первый взгляд, кажется тот факт, что ссылку на a[i] можно записать в виде *(a+i). При анализировании выражения a[i] в языке C оно немедленно преобразуется к виду *(a+i); эти две формы совершенно эквивалентны. Если применить операцию & к обеим частям такого соотношения эквивалентности, то мы получим, что &a[i] и a+i тоже идентичны: a+i - адрес i-го элемента от начала a. С другой стороны, если pa является указателем, то в выражениях его можно использовать с индексом: pa[i] идентично *(pa+i). Короче, любое выражение, включающее массивы и индексы, может быть записано через указатели и смещения и наоборот, причем даже в одном и том же утверждении.
Когда имя массива передается функции, то на самом деле ей передается местоположение начала этого массива. Внутри вызванной функции такой аргумент является точно такой же переменной, как и любая другая, так что имя массива в качестве аргумента действительно является указателем, т.е. переменной, содержащей адрес. Мы можем использовать это обстоятельство для написания нового варианта функции strlen, вычисляющей длину строки: /* возвращает длину строки s */
int strlen(char *s) { int n; for (n = 0; *s != '\0'; s++) n++; return(n); }
Операция увеличения s совершенно законна, поскольку эта переменная является указателем, s++ никак не влияет на символьную строку в обратившейся к strlen функции, а только увеличивает локальную для функции strlen копию адреса.
Описания формальных параметров в определении функции в виде char s[];
и char *s;
совершенно эквивалентны; какой вид описания следует предпочесть, определяется в значительной степени тем, какие выражения будут использованы при написании функции. Если функции передается имя массива, то в зависимости от того, что удобнее, можно полагать, что функция оперирует либо с массивом, либо с указателем, и действовать далее соответвующим образом. Можно даже использовать оба вида операций, если это кажется уместным и ясным.
Имя массива - это константа, представляющая собой указатель на 0-ой элемент массива. Этот указатель отличается от обычных тем, что его нельзя изменить (установить на другую переменную), поскольку он сам хранится не в переменной, а является просто некоторым постоянным адресом.
массив указатель ____________ _____ array: | array[0] | ptr:| * | | array[1] | | | array[2] |<--------- сейчас равен &array[2] | ... |
Следствием такой интерпретации имен массивов является то, что для того чтобы поставить указатель на начало массива, надо писать
ptr = array; или ptr = &array[0]; но не ptr = &array;
Операция & перед одиноким именем массива не нужна и недопустима! Такое родство указателей и массивов позволяет нам применять операцию * к имени массива: value = *array; означает то же самое, что и value = array[0];
Как описывать ссылки (указатели) на двумерные массивы? Рассмотрим такую программу:
#include <stdio.h> #define First 3 #define Second 5 char arr[First][Second] = { "ABC.", { 'D', 'E', 'F', '?', '\0' }, { 'G', 'H', 'Z', '!', '\0' } }; char (*ptr)[Second]; main(){ int i; ptr = arr; /* arr и ptr теперь взаимозаменимы */ for(i=0; i < First; i++) printf("%s\t%s\t%c\n", arr[i], ptr[i], ptr[i][2]); }
Указателем здесь является ptr. Отметим, что у него задана размерность по второму измерению: Second, именно для того, чтобы компилятор мог правильно вычислить двумерные индексы.
Попробуйте сами объявить
char (*ptr)[4]; char (*ptr)[6]; char **ptr;
и увидеть, к каким невеселым эффектам это приведет (компилятор, кстати, будет ругаться; но есть вероятность, что он все же странслирует это для вас. Но работать оно будет плачевно). Попробуйте также использовать ptr[x][y].
Обратите также внимание на инициализацию строк в нашем примере. Строка "ABC." равносильна объявлению
{ 'A', 'B', 'C', '.', '\0' },
Например, используется в функции: Быстрая сортировка (англ. quicksort), часто называемая qsort. В функции qsort указатель на функцию применяется для указания способа сортировки.
Прежде чем вводить указатель на функцию, напомним, что каждая функция характеризуется типом возвращаемого значения, именем и сигнатурой. Напомним, что сигнатура определяется количеством, порядком следования и типами параметров. Иногда говорят, что сигнатурой функции называется список типов ее параметров.
А теперь путём последовательности утверждений придем к обсуждению темы данного раздела урока.
1. При использовании имени функции без последующих скобок и параметров имя функции выступает в качестве указателя на эту функцию, и его значением служит адрес размещения функции в памяти.
2. Это значение адреса может быть присвоено некоторому указателю, и затем уже этот новый указатель можно применять для вызова функции.
3. В определении нового указателя должен быть тот же тип, что и возвращаемое функцией значение, и та же сигнатура.
4. Указатель на функцию определяется следующим образом:
тип_функции (*имя_указателя)(спецификация_параметров);
Например: int (*func1Ptr) (char); - определение указателя func1Ptr на функцию с параметром типа char, возвращающую значение типа int.
Примечание: Будьте внимательны!!! Если приведенную синтаксическую конструкцию записать без первых круглых скобок, т.е. в виде int *fun (char); то компилятор воспримет ее как прототип некой функции с именем fun и параметром типа char, возвращающей значение указателя типа int *. Второй пример: char * (*func2Ptr) (char * ,int); - определение указателя func2Ptr на функцию с параметрами типа указатель на char и типа int, возвращающую значение типа указатель на char.
Иллюстрируем на практике. В определении указателя на функцию тип возвращаемого значения и сигнатура (типы, количество и последовательность параметров) должны совпадать с соответствующими типами и сигнатурами тех функций, адреса которых предполагается присваивать вводимому указателю при инициализации или с помощью оператора присваивания. В качестве простейшей иллюстрации сказанного приведем программу с указателем на функцию:
#include <iostream> using namespace std; void f1(void) // Определение f1. { cout << "Load f1()\n"; } void f2(void) // Определение f2. { cout << "Load f1()\n"; } void main() { void (*ptr)(void); // ptr - указатель на функцию. ptr = f2; // Присваивается адрес f2(). (*ptr)(); // Вызов f2() по ее адресу. ptr = f1; // Присваивается адрес f1(). (*ptr)(); // Вызов f1() по ее адресу. ptr(); // Вызов эквивалентен (*ptr)(); }
Результат выполнения программы:
Load f2() Load f1() Load f1() Press any key to continue
Здесь значением имени_указателя служит адрес функции, а с помощью операции разыменования * обеспечивается обращение по адресу к этой функции. Однако будет ошибкой записать вызов функции без скобок в виде *ptr();. Дело в том, что операция () имеет более высокий приоритет, нежели операция обращения по адресу *. Следовательно, в соответствии с синтаксисом будет вначале сделана попытка обратиться к функции ptr(). И уже к результату будет отнесена операция разыменования, что будет воспринято как синтаксическая ошибка.
При определении указатель на функцию может быть инициализирован. В качестве инициализирующего значения должен использоваться адрес функции, тип и сигнатура которой соответствуют определяемому указателю.
При присваивании указателей на функции также необходимо соблюдать соответствие типов возвращаемых значений функций и сигнатур для указателей правой и левой частей оператора присваивания. То же справедливо и при последующем вызове функций с помощью указателей, т.е. типы и количество фактических параметров, используемых при обращении к функции по адресу, должны соответствовать формальным параметрам вызываемой функции. Например, только некоторые из следующих операторов будут допустимы:
char f1(char) {...} // Определение функции. char f2(int) {...} // Определение функции. void f3(float) {...} // Определение функции. int* f4(char *){...} // Определение функции. char (*pt1)(int); // Указатель на функцию. char (*pt2)(int); // Указатель на функцию. void (*ptr3)(float) = f3; // Инициализированный указатель. void main() { pt1 = f1; // Ошибка - несоответствие сигнатур. pt2 = f3; // Ошибка - несоответствие типов (значений и сигнатур). pt1 = f4; // Ошибка - несоответствие типов. pt1 = f2; // Правильно. pt2 = pt1; // Правильно. char с = (*pt1)(44); // Правильно. с = (*pt2)('\t'); // Ошибка - несоответствие сигнатур. }
Следующая программа отражает гибкость механизма вызовов функций с помощью указателей.
#include <iostream> using namespace std; // Функции одного типа с одинаковыми сигнатурами: int add(int n, int m) { return n + m; } int division(int n, int m) { return n/m; } int mult(int n, int m) { return n * m; } int subt(int n, int m) { return n - m; } void main() { int (*par)(int, int); // Указатель на функцию. int a = 6, b = 2; char c = '+'; while (c != ' ') { cout << "\n Arguments: a = " << a <<", b = " << b; cout << ". Result for c = \'" << c << "\':"; switch (c) { case '+': par = add; c = '/'; break; case '-': par = subt; c = ' '; break; case '*': par = mult; c = '-'; break; case '/': par = division; c = '*'; break; } cout << (a = (*par)(a,b))<<"\n"; //Вызов по адресу. } }
Результаты выполнения программы:
Arguments: a = 6, b = 2. Result for c = '+':8 Arguments: a = 8, b = 2. Result for c = '/':4 Arguments: a = 4, b = 2. Result for c = '*':8 Arguments: a = 8, b = 2. Result for c = '-':6 Press any key to continue
Цикл продолжается, пока значением переменной c не станет пробел. В каждой итерации указатель par получает адрес одной из функций, и изменяется значение c. По результатам программы легко проследить порядок выполнения ее операторов.
Массивы указателей на функции. Указатели на функции могут быть объединены в массивы. Например, float (*ptrArray[4]) (char) ; - описание массива с именем ptrArray из четырех указателей на функции, каждая из которых имеет параметр типа char и возвращает значение типа float. Чтобы обратиться, например, к третьей из этих функций, потребуется такой оператор:
float а = (*ptrArray[2])('f');
Как обычно, индексация массива начинается с 0, и поэтому третий элемент массива имеет индекс 2.
Массивы указателей на функции удобно использовать при разработке всевозможных меню, точнее программ, управление которыми выполняется с помощью меню. Для этого действия, предлагаемые на выбор будущему пользователю программы, оформляются в виде функций, адреса которых помещаются в массив указателей на функции. Пользователю предлагается выбрать из меню нужный ему пункт (в простейшем случае он вводит номер выбираемого пункта) и по номеру пункта, как по индексу, из массива выбирается соответствующий адрес функции. Обращение к функции по этому адресу обеспечивает выполнение требуемых действий. Самую общую схему реализации такого подхода иллюстрирует следующая программа для "обработки файлов":
#include <iostream> using namespace std; /* Определение функций для обработки меню (функции фиктивны т. е. реальных действий не выполняют):*/ void act1 (char* name) { cout <<"Create file..." << name; } void act2 (char* name) { cout << "Delete file... " << name; } void act3 (char* name) { cout << "Read file... " << name; } void act4 (char* name) { cout << "Mode file... " << name; } void act5 (char* name) { cout << "Close file... " << name; } void main() { // Создание и инициализация массива указателей void (*MenuAct[5])(char*) = {act1, act2, act3, act4, act5}; int number; // Номер выбранного пункта меню. char FileName[30]; // Строка для имени файла. // Реализация меню cout << "\n 1 - Create"; cout << "\n 2 - Delete"; cout << "\n 3 - Read"; cout << "\n 4 - Mode"; cout << "\n 5 - Close"; while (1) // Бесконечный цикл. { while (1) { // Цикл продолжается до ввода правильного номера. cout << "\n\nSelect action: "; cin >> number; if (number>>= 1 && number <= 5) break; cout << "\nError number!"; } if (number != 5) { cout << "Enter file name: "; cin >> FileName; // Читать имя файла. } else break; // Вызов функции по указателю на нее: (*MenuAct[number-1])(FileName); } // Конец бесконечного цикла. }
Пункты меню повторяются, пока не будет введен номер 5 - закрытие.