Операции над указателями




Помимо операций доступа по адресу * и получения адреса &, над указателями определены следующие операции:

- сложение с константой,

- вычитание,

- инкремент,

- декремент,

- операции отношений.

Операция доступа по адресу* предназначена для доступа к величине, адрес которой хранится в указателе. Эту операцию можно использовать как для получения, так и для изменения значения величины (если она не объявлена как константа):

char a; // a - переменная типа char

char *ptr; // выделение памяти под указатель ptr

*ptr = ‘@’; // по адресу ptr записано значение @

a = *ptr; // переменной а присвоено значение,
// записанное по адресу ptr

Как видно из примера, конструкцию *указатель можно использовать в левой части оператора присваивания, так как она определяет адрес области памяти. Эту конструкцию можно считать именем переменной, на которую ссылается указатель. С ней допустимы все действия, определенные для величин соответствующего типа.

Арифметические операции с указателями (сложение с константой, вычитание, инкремент и декремент) автоматически учитывают размер типа величин, адресуемых указателями. Эти операции применимы только к указателям одного типа и имеют смысл в основном при работе со структурами данных, последовательно размещенными в памяти, например с массивами.

Инкремент++ перемещает указатель к следующему элементу массива, декремент – к предыдущему. Если указатель на определенный тип увеличивается или уменьшается на константу, то его значение изменяется на величину этой константы, умноженной на размер объекта данного типа. Эта операция производится автоматически.

Разность двух указателей – это расстояние между двумя областями памяти, определяемое в единицах, кратных размеру (в байтах) объекта того типа, к которому отнесены эти указатели. Таким образом, разность указателей, адресующих два смежных объекта одинакового типа, по абсолютной величине всегда равна единице.

Внимание! Суммирование двух указателей недопустимо!

Указатели редко используются с простыми данными вроде отдельных переменных. Преимущества указателей наглядно проявляются при использовании их вместе с массивами, функциями и строками.

 

Указатели и массивы

В Си существует самая тесная связь между указателями и массивами, поэтому лучше эти средства рассматривать вместе.

Как мы знаем, структура массива полностью соответствует структуре оперативной памяти – элементы массива занимают в ней подряд идущие ячейки. Значит, если описан массив

int mass[5];

то в оперативной памяти для его элементов выделяется пять подряд идущих ячеек:

К i -му элементу этого массива можно обратиться, назвав его индекс: mass[i].

Доступ к любому элементу массива, осуществляемый по его индексу (номеру), может быть выполнен при помощи указателя, причем это будет сделано быстрее. Опишем переменную ptr как указатель на данные целого типа:

int *ptr;

В результате присваивания

ptr = &mass[0];

эта переменная будет содержать адрес начального (нулевого) элемента этого массива, то есть указатель ptr будет указывать на элемент mass[0]:

Адрес начального элемента любого массива называется базовым адресом этого массива. Таким образом, сейчас указатель ptr содержит базовый адрес массива mass. Если увеличить значение указателя на единицу, то ptr + 1 будет указывать на следующий элемент массива, то есть на mass[1], ptr + 2 – на элемент mass[2] и так далее. В общем случае, если значение указателя увеличить на k, то можно получить адрес k -го элемента массива mass.

Значит, адрес любого элемента массива равен сумме его базового адреса, который является адресом его начального элемента, и смещения этого элемента от начала массива. Для начального (нулевого) элемента массива это смещение равно нулю, для первого элемента – единице, для второго – двум, для k -го оно равно k. Это верно для массива любого типа. Смысл выражения «увеличить указатель ptr на единицу», как и смысл любой арифметики с указателями, заключается в том, что ptr + 1 указывает на следующий за ptr элемент, а ptr + k – на k -й после ptr элемент массива.

Между индексированием элементов массива и арифметикой с указателями существует очень тесная связь, потому что имя любого массива в Си есть адрес его начального элемента, то есть имя массива является его базовым адресом. Значит,
в нашем примере присваивание

ptr = &mass[0];

можно записать в другом виде:

ptr = mass;

Это будет одно и то же: записи &mass[0] и mass эквивалентны.

Из всего этого следует, что в общем случае запись &mass[k] будет эквивалентна записи (mass + k), а сам k -й элемент массива можно определить как mass[k] или как *(mass + k). С другой стороны, если ptr – указатель, то в выражениях его можно использовать с индексом, то есть запись ptr[k] эквивалентна записи *(ptr + k).

Таким образом, элемент массива в Си разрешается изображать и в виде указателя со смещением, и в виде имени массива с индексом.

Между именем массива и указателем, выступающим в роли имени массива, однако существует различие. Указатель – это переменная, поэтому можно записать ptr = mass или ptr++. Но имя массива не является переменной, и записи типа mass = ptr или mass++ не допускаются.

Помимо рассмотренной операции сложения, над указателями можно выполнять следующие операции:

- складывать и вычитать указатели и целые данные,

- вычитать и сравнивать два указателя, ссылающиеся на элементы одного и того же массива,

- присваивать значение указателя другому указателю того же типа,

- присваивать указателю нуль и сравнивать его с нулем.

Над указателями нельзя выполнять следующие операции:

- складывать два указателя, перемножать их, делить, сдвигать, выделять разряды,

- складывать указатели со значениями типа float и double,

- присваивать указателю одного типа значение указателя другого типа (исключение составляют указатели типа void).

Указатели можно использовать и при работе с многомерными массивами:

int trio[5][2][3];

int *i_ptr;

Описан трехмерный массив trio целого типа и указатель ptr на данные целого типа. Присвоим этому указателю значение базового адреса массива:

i_ptr=&trio[0][0][0];

Необходимо учесть, что для многомерных массивов нельзя использовать операцию присваивания базового адреса указателю в виде

i_ptr=trio;

как это имеет место для векторов (одномерных массивов).

Доступ к j -му элементу i -й строки k -го слоя массива trio может быть осуществлен либо с помощью индексов:

trio[k][i][j]=1;

либо с помощью указателей:

*(i_ptr + k*(2*3) + i*3 + j)=1;

Как и в Паскале, в языке Си запрещается присваивать значения элементов одного массива другому массиву целиком:

float r[2][2], s[2][2];

r = s; // ошибка!

Эти ограничения можно обойти с помощью указателя:

float *f_ptr;

f_ptr = &s[0][0];

r = *f_ptr;

При этом элементам массива r будут присвоены значения соответствующих элементов массива s.

Указатели и функции

В Си имя функции, как и имя массива, эквивалентно ее адресу, то есть адресу того значения, которое возвращается этой функцией. Поэтому можно определить указатель на функцию и далее работать с ним, как с обычной переменной: присваивать, размещать в массиве, передавать в качестве аргумента функции, возвращать как результат функции:

float func(int x, int y); // объявление функции func,

// возвращающей вещественное значение

float (*f_ptr)(); // описание указателя f_ptr

// на любую функцию, возвращающую

// вещественное значение

f_ptr = func; // в указателе – адрес функции func

r = func(a,b); // вызов функции func по ееимени

r = (*f_ptr)(a, b); // вызов функции func по ееадресу

В последнем случае переменной r будет присвоено значение функции, имеющей адрес f_ptr.

При работе с указателями на функции имена этих указателей обязательно заключаются в скобки. Описание

float *f_ptr();

будет трактоваться как объявление функции f_ptr (круглые скобки имеют наивысший приоритет), возвращающей значение указателя на данные вещественного типа.

Аналогично:

r = *f_ptr(a, b);

Переменной r будет присвоено значение, находящееся по адресу, возвращаемому (вычисляемому) функцией f_ptr.

При работе с указателями на функции программисты часто пользуются сложными декларациями (описаниями), которые включают в себя имена, звездочки, круглые и квадратные скобки:

int func (int a, int b); // функция func, возвращающая
// значение целого типа

int (*i_ptr)(); // указатель i_ptr на функцию,
// возвращающую значение целого
// типа

 

int *i_ptr(); // функция i_ptr, возвращающая
// адрес переменной целого типа

int *i_ptr[5]; // массив указателей i_ptr
//
на данные целого типа

int (*i_ptr)[5]; // указатель i_ptr на массив
//
значений целого типа

Для того чтобы правильно читать сложные декларации, необходимо помнить, что наивысший приоритет имеют круглые скобки, затем – квадратные скобки, и в конце – знак *. Чтение описаний осуществляется по правилу “ изнутри наружу ”: начать чтение необходимо с имени и проверить, есть ли справа от него открывающая круглая (тогда это функция) или квадратная (тогда это массив) скобка. Затем следует проверить, есть ли слева от имени звездочка – тогда это указатель, указатель на функцию или массив указателей. Потом снова проверяется наличие открывающей скобки справа и так далее. Если на какой-то стадии чтения справа встретится закрывающая круглая скобка, используемая для изменения порядка интерпретации декларации, то сначала необходимо полностью провести интерпретацию внутри данной пары круглых скобок, а затем продолжать ее справа от закрывающей круглой скобки:

char * (* (*c_ptr) ()) [20];

7 6 4 2 1 3 5

1c_ptr это

2 – указатель на

3 – функцию, возвращающую

4 – указатель на

5 – массив из 20 элементов, которые являются

6 – указателями на

7 – значения типа char.

Примеры:

int *vect[5]; массив vect указателей на значения целого типа: признак типа массива имеет более высокий приоритет, чем признак типа указателя,

int (*vect)[5]; указатель vect на массив значений целого типа,

float *vect(); функция vect, возвращающая указатель на значения вещественного типа: признак типа функции имеет более высокий приоритет, чем признак типа указателя,

float (*vect)(); указатель vect на функцию, возвращающую значение вещественного типа,

double (*vect())[5]; функция vect, возвращающая указатель на массив из пяти элементов типа double.

Массивы указателей на функции удобно использовать при разработке всевозможных меню или программ, управление которыми осуществляется с помощью меню. Для этого действия, предлагаемые на выбор пользователю, оформляются в виде функций, адреса которых помещаются в массив указателей на функции. Пользователь выбирает из меню нужный ему пункт (в простейшем случае вводом номера выбранного пункта), и по этому номеру, как по индексу, из массива выбирается соответствующий адрес функции. Обращение к функции по этому адресу обеспечивает выполнение требуемых действий:

 

#include <stdio.h>

#include <conio.h>

#include <stdlib.h>

int sloshenie(int a, int b);

int vychitanie(int a, int b);

int umnoshenie(int a, int b);

int delenie(int a, int b);

int ostatok(int a, int b);

Int main()

{

int (*i_func[5])(); // вектор указателей на функции,
//
возвращающие целые значения

i_func[0]=sloshenie; // заполнение вектора адресамифункций

i_func[1]=vychitanie; // адрес функции – ее имя

i_func[2]=umnoshenie;

i_func[3]=delenie;

i_func[4]=ostatok;

int x, y, z, nom;

printf("\n first argument =");// ввод аргументовфункций

scanf("%d", &x);

printf("\n second argument =");

scanf("%d", &y);

printf("\n");

puts("|---------------|"); // предлагаемое меню

puts("| Operazii |");

puts("|---------------|");

puts("| 1. sloshenie |");

puts("| 2. vychitanie |");

puts("| 3. umnoshenie |");

puts("| 4. delenie |");

puts("| 5. ostatok |");

puts("|---------------|");

printf("\n vyberite nomer operacii:");

scanf("%d", &nom);

if ((nom<1) || (nom>5)) // защита ввода

{

puts("Error!");

return -1; // аварийное завершениепрограммы

}

z=(*i_func[nom-1])(x,y); // обращение к функции поадресу

printf("\n rezultat =%d", z);

return 0;

}



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2017-03-31 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: