Главная / 2ИСиП / Лекция 7. Указатели и массивы

Лекция 7. Указатели и массивы

1.Основные понятия об указателях

Язык Си отличается от других структурированных немашинных языков широким использованием указателей. В определённой степени именно наличие в языке Си указателей сделало его очень удобным для системного программирования. Указатели обеспечивают доступ к адресам переменных в памяти. Они позволяют работать с символическими адресами (именами переменных, содержащих адреса) и выполнение над адресами ряда операций. С помощью указателей например можно:

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

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

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

Указатель-это переменная или константа, которая содержит значение адреса другой переменной.

 Рисунок1. Графическая интерпретация указателя

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

2.Объявление указателей и основные операции над ними

Указатель-это переменная или константа стандартного типа данных для хранения адреса переменной определённого типа. Тип адресуемой переменной может быть стандартный, перечислимый, структурный, объединение или void. Указатель на тип void может адрес.

Форма объявления переменной типа указатель:

тип [модификатор] *<имя-указателя>
где :
тип-имя типа переменной, адрес которой будет содержать переменная- указатель.(например integer, char, long)
имя-указателя –идентификатор переменной типа указатель.(имя собственное)
*- определяет переменную типа указатель.

Значение переменной-указателя-это адрес некоторой величины, целое без знака. При выводе значения указателя надо использовать формат %u. Указатель содержит адрес первого байта переменной определённого типа. Тип адресуемой переменной, на которую ссылается указатель, определяет объём оперативной памяти, выделяемой переменной, связанной с указателем. Для того, что бы машинной программой обработать (например прочитать или записать) значение переменной с помощью указателя, надо знать адрес её начального (нулевого) байта и количество байтов, которая занимает эта переменная. Ну и указатель естественно содержит эти данные:

Сам указатель содержит адрес нулевого байта этой переменной, а тип адресуемой переменной определяет, сколько байтов, начиная с нулевого (адреса, определённого указателем) занимает это значение.

Примеры объявлений даны на РИС.2

Рисунок2. Примеры объявлений указателей

Язык Си даёт возможность использования адресов переменных программы с помощью основных операций: & и *

&-получение адреса переменной.
*-извлечение значения, расположенного по этому адресу.

С помощью основных операций можно получить значение адреса переменной и использовать косвенную адресацию-получение значения переменной по её адресу.

Операции * и & можно писать вплотную к имени операнда или через пробел.Например: &i, *ptri.

Назначение этих операций:

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

*-имя указателя-получение значения определённого типа по заданному адресу. Определяет содержимое, находящееся по адресу, который содержится в указателе-переменной или указателе-константе. Иначе: косвенная адресация. Косвенная адресация значения с помощью операции * осуществляет доступ к значению по указателю, то есть извлечение значения, расположенного по адресу-содержимому указателя. Операнд *(т.е имя после этого значка) должно быть типа указатель(где-то раньше объявлено).

Оператор присваивания значения адреса указателю(иначе инициализация указателя) имеет вид:

имя указателя_переменной=&имя_переменной

Например:
int *ptri,i; //объявление указателя и переменной типа int
ptri=&i; //ptri получает значение адреса ‘i’

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

Имя_переменной=*имя_указателя

Где имя-указателя –это переменная или константа, которая содержит адрес размещаемого значения, требуемого для переменной левой части оператора присваивания.

Например:
i=*ptri; // ‘i’ получает значение,расположенное по адресу
// содержащемся в указателе‘ptri’

Как и любые переменные, переменная типа указатель ptri имеет адрес и значение.

Схематично взаимосвязь между указателям и адресуемым значением представлениа на рис.3

Рисунок 3.Взаимосвязь указателя, адреса и значения переменной

Указатели можно использовать:
*ptri-значение переменной, находящейся по адресу, содержащемуся в указателяе ptri
ptri-значение адреса переменной
&ptri-адрес местоположения самого указателя

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

int i=123, j, *ptri; //объявление переменных и указателя
ptri=&i; //инициализация указателя(присвоение адреса i)
j=*ptri+1; //переменной i (*ptri) присваивается значение //переменной i и к её содержимому прибавляется единичка.

Следует отметить, что операции & и * более приоритетны, чем арифметические операции

3.Многоуровневая адресация

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

int i=123; //где i-имя переменной
int *pi=&i; //pi –указатель на переменную
int **ppi=π //ppi-указатель на ‘указатель на переменную’
int ***pppi=&ppi; //pppi-указатель на ‘указатель на ‘указатель на переменную’’.

Для обращения к значениям с помощью указателей можно применять следующее правило, жёстко связывающее форму обращения с объявлением этих указателей:

  • Полное количество звёздочек косвеной адресации, равное количеству звёздочек при объявлении указателя, определяет значение переменной.
  • Уменьшение количества звёздочек косвенной адресации добавляет к имени переменной слово

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

Пример, поясняющий это правило дан на РИС.4

Рисунок 4.Соответствие между количеством уточнений (*) и результатом обращения к значению с помощью указателя.

Где У.К.А.-уровень косвеной адресации

Пример, иллюстрирующий многоуровневую косвенную адресацию

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

Язык Си предоставляет возможности для выполнениянад указателями операций присваивания, целочисленой арифметики и сравнений.

На языке Си можно:

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

Переменной-указателю можно задать значение с помощью одного из способов:

  • Присвоить указателю адрес переменной, имеющей место в оперетивной памяти, или нуль, например
    : ptri=&i; ptri=NULL;
  • Объявить указатель вне функции (в том числе main) либо в любой функции, снабдив его описателем stastic, при этом начальным значением указателя является нулевой адрес (NULL)
  • Присвоить указателю значение другово указателя, который к этому времени уже инициализирован (имеет определённое значение), например: ptri=ptrj; -это двойное указание одной и той же переменной.
  • Присвоить переменной-указателю значение с помощью функций malloc и calloc.

Изменение значений указателя можно производить с помощью операций +, ++, -, —. Бинарные операции (+ и -) можно выполнять над указателями, если оба указателя ссылаются на переменные одного типа, так как объём оперативной памяти для различных типов данных может быть разный. Например, значение типа char занимает 1(один) байт, значение типа int занимает 2(два) байта, а под значение типа float выделяется аж 4(четыре) байта. Добавление 1 к указателю добавит ‘квант памяти’ ,то есть количество байтов, которое занимает одно значение адресуемого типа. Для указателя на элементы массива это означает, что осуществляется переход к адресу следующего элемента массива, а не следующего байта. То есть значение указателя при переходе от элементак элементу массива целых чисел будет увеличиваться на 2, а типа float-на 4.Более подробно это продемонстрированно на РИС.5

Рисунок 5.Арифметические действия над различными типам указателей.

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

int mas[10],*ptrm;
ptrm=&mas[0];
*prtm==mas[0]==*(mas+0) ; -значение нулевого элемента массива mas
*(ptrm+i)==mas[i]==*(mas+i); —значение i-го элемента массива mas

А операции над элементами массива mas можно представить в виде:
*mas+2==mas[0]+2; *(mas+i)-3==mas[i]-3;

А вот задачка, предложенная на собеседовании в одной софтверной фирме:*(&(mas[i+1])+2)++;

  • ptrm==&mas[i+1]; упрощаем данное выражение, i здесь не играет роли
  • ptrm+2==&(mas[i+1])+2;

здесь указатель переводится на 2 элемента вперёд

  • *ptrm++==(*ptrm=*ptrm+1); здесь содержимое ячейки массива извлекается и к нему прибавляется единичка

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

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

Например в выражении *p++ сначала выполняется префиксная операция над указателем ,то есть определяется значение *p-содержимое, расположенное по адресу px, а затем выполняется посфиксная операция ++ увелечение значения указателя на квант памяти, то есть на 2 байта (если указатель типа int)

А, например в выражениии (++(*p)+2) сначала:

  • *p -так как префиксные операции выполняются справа налево.
  • *p=*p+1

-самая ‘левая’ префиксная операция

  • +2 -выполнение посфиксной операции

5.Проблемы, связанные с указателями

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

  • Попытка работать с неинициализированными указателям, то есть с указателем, не содержащим адреса оперативной памяти (ОП), выделенной переменной.
  • Потеря ссылки, то есть значения указателя из-за присваивания ему нового значения до освобождения ОП, которою он адресует.
  • Неосвобождение ОП, запрошенной с помощью функции malloc

Попытка возвратить в качестве результата работы функции адрес локальной переменной (класса auto).

Создание и уничтожение динамических многомерных массивов

Как правило, работа с такими массивами осуществляется следующим образом:

const unsigned int DIM1 = 3;
const unsigned int DIM2 = 5;

int main() {

    int **ary;    // (1)

    // создание
    ary = new int * [DIM1];    // массив указателей (2)
    for (int i = 0; i < DIM1; i++) {   // (3)
        ary[i] = new int [DIM2];     // инициализация указателей
    }

    // работа с массивом
    for (int i = 0; i < DIM1; i++) {
        for (int j = 0; j < DIM2; j++) {
            ary[i][j] = (i + 1) * 10 + (j + 1);
        }
    }

    for (int i = 0; i < DIM1; i++) {
        for (int j = 0; j < DIM2; j++) {
            cout << setw(4) << ary[i][j];
        }
        cout << endl;
    }

    // уничтожение
    for (int i = 0; i < DIM1; i++) {
        delete [] ary[i];
    }
    delete [] ary;

    return 0;
}

(1) Для доступа к двумерному массиву объявляется переменная ary типа указатель на указатель на тип (в данном случае это указатель на указатель на int).

(2) Переменная инициализируется оператором new, который выделяет память для массива указателей на int.

(3) В цикле каждый элемент массива указателей инициализируется оператором new, который выделяет память для массива типа int.

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

Работа с динамическим многомерным массивом синтаксически полностью совпадает с работой с многомерным C-массивом.

Пример кода для трёхмерного массива:

const unsigned int DIM1 = 3;
const unsigned int DIM2 = 5;
const unsigned int DIM3 = 2;

int main() {

    int ***ary;

    int cnt = 1;

    // создание
    ary = new int ** [DIM1];
    for (int i = 0; i < DIM1; i++) {
        ary[i] = new int * [DIM2];
        for (int j = 0; j < DIM2; j++) {
            ary[i][j] = new int [DIM3];
        }
    }

    // работа с массивом
    for (int i = 0; i < DIM1; i++) {
        for (int j = 0; j < DIM2; j++) {
            for (int k = 0; k < DIM3; k++) {
                ary[i][j][k] = cnt++;
                cout << setw(4) << ary[i][j][k];
            }
            cout << endl;
        }
        cout << endl;
    }

    // уничтожение
    for (int i = 0; i < DIM1; i++) {
        for (int j = 0; j < DIM2; j++) {
            delete [] ary[i][j];
        }
        delete [] ary[i];
    }
    delete [] ary;

    return 0;
}

Где собака порылась

Работа с динамическим многомерным массивом синтаксически полностью совпадает с работой с многомерным C-массивом. (Цитирую предыдущий раздел.) Синтаксически — да, но между этими массивами есть глубокое различие, о котором начинающие программисты часто забывают.

Во-первых, для динамического массива выделяется другой объём памяти.

Если посчитать, сколько памяти будет выделяться для двумерного массива из примера выше, то получится: первый оператор new выделил память для 3 указателей, второй оператор new в цикле трижды выделил память для 5 элементов типа int. Т.е. получилось, что выделили памяти для 15 значений типа int и для 3 значений типа указатель на int. Для C-массива компилятором была выделена память только для 15 значений типа int. (Всяческие выравнивания и прочие оптимизации не учитываем!)

Во-вторых, память, выделенная для динамического массива, не непрерывна. Следовательно, хак №1 (обращение с двумерным массивом как с одномерным) работать не будет.

В-третьих, передача многомерных массивов в функции и работа с ними будет отличаться для динамических массивов и C-массивов.

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

Динамический многомерный массив НЕ является C-массивом.

Парадоксально, но факт, что наиболее близким родственничком для этих неродных близнецов, является хак №2, реализующий работу с многомерным массивом посредством одномерного массива (см. раздел Хаки). Все три вышеперечисленных различия для него неактуальны.

Стоит отметить, что массив указателей на массивы — структура более гибкая, чем двумерный C-массив. Например, для массива указателей на массивы размеры массивов могут быть разными, или какой-то массив может вообще отсутствовать. Наиболее распространённым примером является «массив строк», т.е. массив указателей на массивы типа char (пример — см. в следующем разделе).