Урок 7.1.1. Описание указателей

Урок 7.1.4. Операция косвенной адресации

7.1.4. Операция косвенной адресации

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

*<адресное_выражение>

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

int q,b,*pq;
q=5; pq=&q;
b=*pq;

то в результате его выполнения переменная b примет значение 5. В последнем операторе b=*pq; по адресу, записанному в указателе pq, извлекается значение переменной q, т,е. числo 5, и присваивается переменной b. В данном случае мы извлекли значение не прямым обращением к переменной путем использования ее имени – q, а косвенно, путем использования ее адреса, имеющегося в указателе рq.

Выражение *pq записано справа от знака присваивания и является правоопределенным (r-value, right value) вариантом этого выражения. Как в указатель pq попал адрес переменной q – понятно, в результате выполнения предыдущего оператора рq=&q;.

Таким образом, в этом контексте операция косвенной адресации трактуется как "обратиться к данному по адресу, извлечь данное по адресу", и ее значением является данное, размещенное по этому адресу. Но как же определяется, сколько байтов данного нужно извлечь из адреса? Очень просто. К моменту использования указателя его тип всегда должен быть определен, в данном случае он имеет тип int и, следовательно, нужно извлечь два байта данного. Рассмотрим другой, несколько видоизмененный пример:

int q,b,*pb;
q=5; pb=&b;
*pb=q;

в котором операция косвенной адресации использована слева от операции присваивания.

Это случай так называемого левоопределенного выражения (l-value, left value) вида: *<адресное_выражение>. Теперь левосторонняя операция косвенной адресации с учетом операции присваивания трактуется как "записать данное по адресу", и в результате выполнения этого фрагмента программы переменная b примет значение 5, как и в предыдущем случае.

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

Нередко операцию косвенной адресации называют операцией разадресации, имея ввиду, что фактически программа работает не с адресом памяти, а с содержимым памяти по этому адресу.

Естественно, что перед использованием операции косвенной адресации в программе должны быть известны адрес объекта, тип объекта и его значение. Только в этом случае операция косвенной адресации будет выполнена корректно.

Тип объекта не может быть типом void – пустым типом, но значение объекта может быть "пустым", т.е. еще не известным, если ему присваивается значение с использованием левосторонней операции косвенной адресации. Мы уже приводили пример описания указателя на данные, тип которых заранее не определен: void *p5;

Такой указатель совместим с указателем на любой тип данных, т.е. ему можно присвоить адрес объекта любого типа, но перед использованием указателя, объявленного как void, он обязательно должен быть специфицирован типом объекта, на который он указывает, например:

void *p5;
double r,q;
q=1.85e-12;
p5=&q;
r=*((double *)p5);

Здесь использована операция преобразования типа (double *), с тем чтобы указатель на неопределенный тип данных р5 был преобразован к указателю на тип данных double, потому что операцией косвенной адресации мы желаем получить доступ к данному именно этого типа.
Было бы ошибкой записать последний оператор примера как r=*((double)p5);, ибо в этом случае указатель р5 приводился бы к числу с плавающей точкой типа double, а не к указателю на тип double.

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

Урок 7.1.5. Инициализация указателей

7.1.5. Инициализация указателей

Инициализации указателей корректными значениями адресов памяти необходимо уделять повышенное внимание. Дело в том, что указатели можно инициализировать точно так же, как и обычные переменные, например:

int *р1 = (int *)1242, *р2 = (int *)1246;
// где int* операция преобразования целого значения
// к указателю на целое значение (к адресу)

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

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

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

float r, b;
float *ukr = &r, *ukb = &b;

В таком описании указателей с инициализацией ничего странного нет, ибо, как уже известно, &r и &b являются константами типа указатель. Эти же указатели можно инициализировать и в ходе выполнения программы адресами уже описанных переменных, например так:

float r, b;
float *ukr, *ukb;
. . . . . . . . .
 ukr = &r; ukb = &b;

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

char *uk_simv_rus, *uk_simv_lat;
char simvsmoll, simvlarge;
 . . . . . . . . .
 uk_simv_lat = &simvsmoll;
 uk_simv_rus = uk_simv_lat;

Обратим внимание на то, что в этом случае разные указатели содержат адреса одной и той же переменной. Значит, к переменной simvsmoll можно обратиться как через указатель uk_simv_rus так и через указатель uk_simv_lat, что зачастую предоставляет программисту дополнительные удобства.

Третий способ: использовать одну из встроенных функции распределения памяти, например, malloc. Обращение к функции malloc имеет вид:

malloc(<размер>);

Функция malloc выделяет (резервирует) из области “куча” (heap) память длиной <размер> байтов, где <размер> задается выражением типа unsigned. Функция возвращает адрес выделенной памяти в виде константы-указателя типа void.

Если в памяти нет под кучу свободного места требуемого размера, то в качестве результата будет выдан адрес 0 (NULL). Куча (engl. heap)– это память, выделяемая динамически или запрашиваемая статически у операционной системы. Эта память используется для размещения объектов, динамически созданных программой.)

Инициализацию указателя можно осуществить следующим образом:

int *x = (int *)malloc(sizeof(int)); .

Для такого способа инициализации полезно задать себе ряд вопросов, задуматься над ними и получить аргументированные ответы. Рассуждать будем так: это оператор описания, ибо он начинается с ключевого слова, задающего <тип> . Мы знаем, что при описании переменных допустима их инициализация только константными выражениями.

Исходя из такой посылки, сформулируем наши вопросы и дадим на них ответы. Вопрос первый: является ли выражение справа от знака присваивания константным? Да, является, так как функция возвращает адрес выделенной памяти и можно считать, что обращение к функции есть именованная константа-указатель, значение которой устанавливается в момент применения функции.

Вопрос второй: почему в константном выражении использована операция преобразования типа (int *)? Потому что возвращается константа-указатель на неопределенный тип void, а мы инициализируем указатель на данные типа int. Следовательно, нам нужно преобразовать константу-указатель на неопределенный тип к константе-указателю на целый тип.

И, наконец, последний, третий вопрос: почему <размер> задан операцией sizeof(int)? Ответ: для того, чтобы обеспечить мобильность программы. В самом деле, если мы запишем этот оператор в виде int*x=(int*)malloc(2); , то при использовании компьютера другой архитектуры, где, например, данные типа int представляются четырехбайтными словам памяти, такая программа может оказаться неработоспособной, т.к. для четырехбайтного целого резервируется два байта памяти.

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

int *x;
. . . .
x = (int *)malloc(sizeof(int);
if (x != 0) //если malloc вернула 0, то память не выделилась
{
 //действия программы, если память выделена
*х =- 5244;
 //в выделенную память послали -5244
}
else {
// действия программы, если память не выделена 
};

В языке С++ для выделения памяти используется операция new:

new <тип>
new <тип>[<количество элементов>]

Например:

int *x;
x = new int;     //выделить память под хранение данного типа int
x = new int[10]; //выделить память под хранение 10-ти данных типа int

Если операция new возвращает 0 (NULL), то это означает, что память не выделилась.

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

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

free(<указатель>);

где <указатель> должен содержать в себе адрес памяти, которая должна быть освобождена. Чтобы уничтожить в памяти значение -5244, динамически созданное в области кучи, как показано в предыдущем примере, нужно записать оператор free(x); .

В языке С++ для освобождения динамически выделенной памяти используется операция delete:

delete <указатель>;

delete [ ] <указатель>;

Например:

int * p;
int i = 10;
 
p= new int[i]; //выделить память под массив данных
if(p != NULL) printf("Память выделена");
else printf("Свободной памяти нет");
delete[] p;    //освободить память массива данных по адресу р
 
int *x;
x = new int;
delete x;  // освободить память по адресу x

 

Урок 7.2.1. Описание массивов

7.2.1. Описание массивов

Как и в большинстве других языков программирования, в Си под массивом понимается совокупность однородных данных, рассматриваемых как нечто единое. Математическими аналогами программистского понятия массив является, например, матрица, которая в программе задается в виде двухмерного массива, или вектор, задаваемый одномерным массивом. Массив – это такой объект программы, который характеризуется именем, размерностью, количеством элементов по каждому измерению и типом значений его элементов.

В программе на Си описание массивов осуществляется оператором, имеющим следующую общую форму записи:

<тип> <имя>[<размер1>][<размер2>]...[<размерN>];

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

Чаще всего в качестве <тип> используются ключевые слова, такие, как int, float, char и другие ключевые слова и словосочетания, задающие основные и производные типы данных, например, указатели, структуры и т.д.; <имя> – это <идентификатор>, задающийся по усмотрению программиста и являющийся именем массива. Имя массива используется для доступа к элементам массива; <размер> определяет количество элементов массива по каждому измерению и записывается целой беззнаковой константой.

Количество конструкций [<размер>] определяет размерность массива. При описании одномерного массива указывается только <размер1>, двухмерного массива <размер1> и <размер2> и т.п. для многомерных массивов. Многомерные массивы рассматриваются в Си как одномерные, элементами которых могут быть любые объекты, в том числе и массивы.

Примеры описания массивов:

int v[120];                 v – одномерный массив из 120-ти целых чисел;

float mt[7][9];           mt – двумерный массив из 63-х (7*9) чисел с  плавающей точкой;

char str[25];             str – массив из 25-ти символов;

char *u[10];              u – массив указателей на объекты типа char;

int *pr[2][3];              pr – двухмерный массив из 6-ти указателей на целые данные;

int (*r)[5][6];              r – указатель на данные целого типа, сгруппированные в массивы 5*6;
обратите внимание, что r в данном случае не является массивом.

Доступ к элементу массива обеспечивается использованием имени массива, вслед за которым в квадратных скобках записываются координаты элемента (индексы) по соответствующим измерениям. Индексы однозначно определяют положение элемента в массиве.

Нижняя граница индекса по каждому измерению массива имеет значение 0, а верхняя имеет значение <размер1>. В качестве индекса выступают выражения, значения которых приводятся к целому типу. Рассмотрим фрагмент программы:

int vect[10];
int k;
for(k = 0; k <= 9; k++)  
      vect[k] = k*k;

В этом фрагменте описан одномерный массив с именем vect, содержащий 10 элементов типа int, первый элемент массива имеет индекс 0, второй – 1, третий – 2 и т.д., последний элемент имеет индекс 9. Доступ к элементам массива осуществляется путем использования вышеописанной языковой конструкции vect[k], которая называется индексированной переменной, или переменной с индексом.

Здесь уместно обратить внимание на то, что сами по себе массивы представляют векторные данные, но индексированная переменная есть скаляр. В результате выполнения этого фрагмента элементу vect[0] будет присвоено значение 0, элементу vect[1] значение 1, vect[2] значение 4 и т.д., до последнего элемента массива vect[9], которому будет присвоено значение 81.

Рассмотрим, каким образом определяются координаты элементов в многомерных массивах. Многомерные массивы Си трактуются как одномерные массивы, элементами которых, в свою очередь, являются массивы.

С этой точки зрения препарируем следующее описание массива: int m[2][3][2]; .

Рассуждаем так: константа 2 в первых квадратных скобках определяет базовый одномерный массив m из двух элементов m[0] и m[1]. Вот они:

M_g7_2
Константа 3 в следующих квадратных скобках говорит о том, что каждый из этих элементов состоит, в свою очередь, из трех элементов:

M_g7_2_2
Элементы, которые находятся в m[0], это элементы m[0][0], m[0][1], m[0][2], а элементы, которые содержатся в m[1], это элементы m[1][0], m[1][1] и m[1][2]. Константа 2 в последних квадратных скобках говорит о том, что каждый из предыдущих элементов состоит, в свою очередь, из двух элементов типа int:

M_g7_2_3

Таким образом, элементы трехмерного массива отображаются в линейной памяти одномерным массивом, доступ к элементам которого осуществляется путем использования индексированной переменной с тремя индексами. В данном случае элементы массива расположены в памяти в следующей последовательности: m[0][0][0], m[0][0][1], m[0][1][0], m[0][1][1], m[0][2][0], m[0][2][1], m[1][0][0], m[1][0][1], m[1][1][0], m[1][1][1], m[1][2][0], m[1][2][1]. Обратите внимание - чем дальше индекс от имени массива, тем "быстрее" он меняется и элементы массива попадут в память построчно из первого массива, а затем и из второго.

Чтобы обратиться к элементу n-мерного массива, нужно использовать индексированную переменную с n индексами, например, оператор m[1][2][1]=15; присваивает значение 15 последнему элементу рассмотренного массива, а оператор m[0][0][1]=m[1][2][1]; переприсваивает это значение второму элементу.

Для случая двухмерных массивов полная аналогия с матрицами устанавливается следующим образом: при описании массивов первая константа в квадратных скобках задает количество строк, а вторая – количество столбцов матрицы; для индексированной переменной первый индекс определяет номер столбца, а второй – номер строки, на пересечении которых находится элемент матрицы.

Для тех, кто предпочитает мыслить образами, можно предложить следующие сравнения. Одномерный массив  - это полка, на которой стоят элементы. Двумерный массив  - это прямоугольная таблица или одномерный массив одномерных массивов. Трехмерный массив - это ящик, в который помещены таблицы или двумерный массив двумерных массивов. Четырехмерный массив - это полка, на которой стоят ящики или одномерный массив трехмерных массивов, который в свою очередь является одномерным массивом двумерных массивов.

Translate Переводчик

Подписка на новости

SmartResponder.ru
Ваш e-mail: *
Ваше имя: *

Хостинг для Wordpress сайтов