- •Указатели
- •Типы указателей. Объявление указателей.
- •1.1.1. Указатель данных
- •1.1.2. Указатель функции
- •Операции с указателями
- •Тема № 8. Указатели и массивы в языке с.
- •Инициализация массивов
- •Объявление массива
- •Одномерные (линейные) массивы
- •Многомерные массивы
- •Работа с массивами
- •Доступ к элементу массива
- •Обработка массивов
- •Ввод/вывод массивов
- •Примеры программ обработки массивов
- •4.10. Указатели
1.1.2. Указатель функции
С функцией можно проделать только две вещи: вызвать её и получить её адрес. Указатель, полученный взятием адреса функции, можно затем использовать для вызова функции.
Указатель функции содержит адрес в сегменте кода, по которому располагается исполняемый код функции.
Указатель функции имеет тип «указатель функции, возвращающей значение заданного типа и имеющей аргументы заданного типа».
Синтаксис объявления указателя функции:
<тип> (*<имя>) (<список_типов_аргументов>);
Пример 1.
int (*fun) (double, double);
задает указатель с именем fun на функцию, возвращающую значение типа int и имеющую два аргумента типа double.
Пример 2.
void f (int x) { ... } |
|
void (*pf)(int); |
// Указатель на функцию. Скобки обязательны! |
void g(){ |
|
pf = &f; |
// pf указывает на функцию f |
pf(0); |
// Вызов функции f через указатель pf |
} |
|
Компилятор распознаёт, что pf является указателем и вызывает функцию, на которую он указывает. То есть, разыменование указателя на функцию при помощи операции * необязательно. Аналогично, необязательно пользоваться операцией & для получения адреса функции.
Параметры указателей на функцию объявляются точно так же, как и параметры самих функций. При присваивании типы функций должны в точности совпадать.
typedef void (*PF)(int); |
// Для объявления типа «указатель на функцию» можно использовать объявление typedef |
PF pf; |
// Объявляем сам указатель на функцию, используя предварительно определённый тип |
void f1(int x) { ... } int f2(int x) { ... } void f3(char x) { ... } |
|
void f (){ |
|
pf = &f1; |
// Правильно |
pf = &f2; |
// Ошибка - не тот возвращаемый тип |
pf = &f3; |
// Ошибка - не тот тип параметра |
} |
|
Правила передачи параметров при вызове функций через указатель те же самые, что и при непосредственном вызове функций.
Операции с указателями
С указателями можно выполнять следующие операции: разадресация (*), присваивание, сложение с константой, вычитание, инкремент (++), декремент (––), сравнение, приведение типов. При работе с указателями часто используется операция получения адреса (&).
Существуют две операции, которые имеют отношение к работе с указателями. Этими операциями являются:
операция взятия адреса (адресация) &;
операция взятия значения по адресу (косвенная адресация или разыменование) *.
int a, *p; |
|
p = &a; |
// Переменной p присваивается адрес переменной a |
*p = 0; |
// Значение по адресу, находящемуся в переменной p (т.е. значение переменной а), становится равным 0 |
Ссылка является альтернативным именем объекта. Ссылка – это объект, который синтаксически выглядит как переменная, а по семантике является адресом. Объявление ссылки, кроме случаев, когда ссылка является параметром функции, возвращаемым функцией значением или членом класса, должно содержать инициализатор. Далее все операции производятся не над самой ссылкой, а над тем объектом, на который она указывает.
int a = 10; |
|
int &r = a; |
// Объявляем и инициализируем ссылку |
r++; |
// Значение переменной а становится 11 |
void f(double &a) { a += 3.14; } |
|
double d = 0; |
|
f(d); |
// d = 3.14 |
int v[20]; |
|
int& f(int i) { return v[i]; } |
|
f(3) = 7; |
// Элементу массива v[3] присваивается 7 |
На первый взгляд, ссылка является удобной заменой указателю, но она затрудняет понимание программы из-за несовпадения синтаксиса и семантики ссылки. Однако ссылки могут быть полезны для того, чтобы не передавать по значению (и не копировать) параметр функции, который имеет большой размер. В том случае, если мы не собираемся менять этот параметр внутри функции, можно объявить ссылку с модификатором const. В этом случае мы будем гарантированы, что параметр не изменится, вместо большого объекта будет передаваться его адрес, а для пользователя всё будет выглядеть как передача параметра по значению.
class X { ... }; |
// Описание большого класса |
int f (const X& x) { ... } |
|
Операция разадресации, или разыменования, предназначена для доступа к величине, адрес которой хранится в указателе. Эту операцию можно использовать как для получения, так и для изменения значения величины (если она не объявлена как константа):
char a; //переменная типа char
char * p = new char; //выделение памяти под указатель и под динамическую переменную типа char
*p = 'Ю'; a = *p; //присваивание значения обеим переменным
На одну и ту же область памяти может ссылаться несколько указателей различного типа. Примененная к ним операция разадресации даст разные результаты. Например, программа
#include <stdio.h>
int main() {
unsigned long int A = 0Xсс77ffaa;
unsigned int* pint = (unsigned int *) &A;
unsigned char* pchar = (unsigned char *) &A;
printf(" | %x | %x |", *pint, *pchar);
return 0;
}
на IBM PC выведет на экран строку:
| ffaa | aa |
В примере при инициализации указателей были использованы операции приведения типов. Синтаксис операции явного приведения типа прост: перед именем переменной в скобках указывается тип, к которому ее требуется преобразовать.
При смешивании в выражении указателей разных типов явное преобразование типов требуется для всех указателей, кроме void*. Указатель может неявно преобразовываться в значение типа bool.
Присваивание без явного приведения типов допускается только указателям типа void* или если тип указателей справа и слева от операции присваивания один и тот же.
Присваивание указателей данных указателям функций (и наоборот) недопустимо.
Арифметические операции с указателями (сложение, вычитание, инкремент и декремент) автоматически учитывают размер типа величин, адресуемых указателями. Эти операции применимы только к указателям одного типа и имеют смысл в основном при работе со структурами данных, последовательно размещенными в памяти, например, с массивами.
Инкремент перемещает указатель к следующему элементу массива, декремент — к предыдущему.
Фактически значение указателя изменяется на величину sizeof(тип).
Разность двух указателей — это разность их значений, деленная на размер типа в байтах. Суммирование двух указателей не допускается.
При записи выражений с указателями следует обращать внимание на приоритеты операций. В качестве примера рассмотрим последовательность действий, заданную в операторе
*p++ = 10;
То же самое можно записать подробнее:
*p = 10;
p++;
Выражение (*p)++, напротив, инкрементирует значение, на которое ссылается указатель.
Унарная операция получения адреса & применима к величинам, имеющим имя и размещенным в оперативной памяти. Нельзя получить адрес скалярного выражения, неименованной константы или регистровой переменной.
Адресная арифметика
Над указателями можно производить следующие действия:
складывать указатель и целое число;
вычитать из указателя целое число;
вычитать из указателя указатель – получается целое число.
При этом прибавляемое/вычитаемое или полученное целое число обозначает не количество байт, а количество элементов того типа, на который указывает указатель, т.е. это число умножается или делится на размер типа.
Вычитание указателей друг из друга определено только в том случае, если оба указателя указывают на элементы одного и того же массива (хотя язык не позволяет быстро проверить, так ли это). Результатом вычитания одного указателя из другого будет количество элементов массива (целое число) между этими указателями. В результате прибавления целого числа к указателю и вычитания целого числа из указателя получается новый указатель. Если полученный таким образом указатель не указывает на элемент того же массива (или на элемент, следующий за последним), что и исходный указатель, то результат его использования не определён.
Указатели можно использовать для обработки массивов.
int a[100], n, *end, *p; |
|
end = a + n; |
// n - количество элементов массива a. Имя массива является адресом его начала. // Таким образом, end - адрес элемента, находящегося после последнего элемента массива. |
for (p = a; p < end; p++) |
|
printf("%4d", *p); |
|
Из того, что можно вычитать из указателя целое число, следует, что возможно использование отрицательных чисел в операции [ ].
int a[N]; int *endA = a + N - 1, i; for (i = 0; i < N; i++) printf("%4d", endA[-i]); |
Ссылки
Ссылка представляет собой синоним имени, указанного при инициализации ссылки. Ссылку можно рассматривать как указатель, который всегда разыменовывается. Формат объявления ссылки:
тип &имя;
где тип — это тип величины, на которую указывает ссылка, & — оператор ссылки, означающий, что следующее за ним имя является именем переменной ссылочного типа, например:
int kol;
int& pal = kol; // ссылка pal — альтернативное имя для kol
const char& CR = '\n'; // ссылка на константу
Переменная-ссылка должна явно инициализироваться при ее описании, кроме случаев, когда она является параметром функции, описана как extern или ссылается на поле данных класса.
После инициализации ссылке не может быть присвоена другая переменная.
Тип ссылки должен совпадать с типом величины, на которую она ссылается.
Не разрешается определять указатели на ссылки, создавать массивы ссылок и ссылки на ссылки.
Ссылки применяются чаще всего в качестве параметров функций и типов возвращаемых функциями значений. Ссылки позволяют использовать в функциях переменные, передаваемые по адресу, без операции разадресации, что улучшает читаемость программы.
Операция над ссылкой приводит к изменению величины, на которую она ссылается.
