Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Lek9-10.doc
Скачиваний:
4
Добавлен:
15.11.2018
Размер:
217.6 Кб
Скачать

Лекция 9,10: УКАЗАТЕЛИ и ССЫЛКИ

Указатели

Указатель – это производный тип, который представляет собой адрес какого-либо значения. В языке Си++ используется понятие адреса переменных. Работа с адресами досталась Си++ в наследство от языка Си.

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

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

тип (*имя) ( список_типов_аргументов );

Например, объявление:

int (*fun) (double, double);

задает указатель с именем fun на функцию, возвращающую значение типа int и имеющую два аргумента типа double.

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

тип *имя;

где тип может быть любым, кроме ссылки и битового поля, причем тип может быть к этому моменту только объявлен, но еще не определен.

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

int *a, b, *c;

описываются два указателя на целое с именами a и c, а также целая переменная b.

Размер указателя зависит от модели памяти. Можно определить указатель на указатель, и т.д.

Предположим, что в программе определена переменная типа int:

int x;

Можно определить переменную типа "указатель" на целое число:

int* xptr;

и присвоить переменной xptr адрес переменной x:

xptr = &x;

Операция &, примененная к переменной, – это операция взятия адреса. Операция *, примененная к адресу, – это операция обращения по адресу. Таким образом, два оператора эквивалентны:

int y = x;

// присвоить переменной y значение x

int y = *xptr;

// присвоить переменной y значение,

// находящееся по адресу xptr

С помощью операции обращения по адресу можно записывать значения:

*xptr = 10;

// записать число 10 по адресу xptr

После выполнения этого оператора значение переменной x станет равным 10, поскольку xptr указывает на переменную x.

Указатель – это не просто адрес, а адрес величины определенного типа. Указатель xptr – адрес целой величины. Определить адреса величин других типов можно следующим образом:

unsigned long* lPtr;

// указатель на целое число без знака

char* cp;

// указатель на байт

Complex* p;

// указатель на объект класса Complex

Можно определить указатель на любой тип, в том числе на функцию или метод класса.

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

int* hardwareRegiste =0x80000;

*hardwareRegiste =12;

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

Связь между массивами и указателями

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

Пример. Определение

int a [ 10 ] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

дает нам массив из 10 элементов типа int. Такой массив всегда хранится в памяти последовательно:

a[0] a[1] a[2] …….. a[9]

а

Если мы определим указатель на int

int *ptr_a;

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

ptr_a = &a;

ptr_a

a

a[0] a[9]

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

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

Очевидно, что ptr_a + 1 будет указывать на следующий элемент массива а. т.е. на a[1]. Аналогично ptr_a + 2 указывает на следующий за ним элемент a[2] и т.д.

Таким образом:

если ptr_a указывает на a [ 0 ], то *( ptr_a + i ) есть содержимое a [ i ] (в нашем случае 2)

Идентификатор массива является адресом первого элемента, входящего в этот массив, т.е. имеет место равенство

a == &a [ 0 ]

а присваивание ptr_a = a [ 0 ] можно короче записать так:

ptr_a = a;

a [ i ] можно эквивалентно записать как *(a + i) – это даст нам элемент массива а с индексом i, т.е.:

a [ i ] = *(a + i);

Начальный элемент массива можно просто записать как *а, т.е.:

a [ 0 ] = = *a == 1 ; // в нашем случае

Предположим, имеется массив из 100 целых чисел. Запишем двумя способами программу суммирования элементов этого массива:

long array[100];

long sum = 0;

for (int i = 0; i < 100; i++)

sum += array[i];

То же самое можно сделать с помощью указателей:

long array[100];

long sum = 0;

for (long* ptr = &array[0]; // long* ptr = array

ptr < &array[99] + 1; ptr++)

sum += *ptr;

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

for (long* ptr = array;

ptr < &array[99] + 1; ptr++)

sum += *ptr;

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

При использовании многомерных массивов указатели позволяют обращаться к срезам или подмассивам. Если мы объявим трехмерный массив exmpl:

long exmpl[5][6][7]

то выражение вида exmpl[1][1][2] – это целое число, exmpl[1][1] – вектор целых чисел (адрес первого элемента вектора, т.е. имеет тип *long), exmpl[1] – двухмерная матрица или указатель на вектор (тип (*long)[7]). Таким образом, задавая не все индексы массива, мы получаем указатели на массивы меньшей размерности.

Отличия между доступом по имени массива и по указателю

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

1). При определении массива выделяется блок памяти, размер которого достаточен для хранения описанного массива.

Идентификатор массива инициализируется адресом этой памяти. Этот адрес не может меняться внутри программы. Поэтому а нельзя изменять внутри программы и такая запись будет ошибочной:

а++; // а менять нельзя

А так писать можно:

*( а + i ) или a [ i ]

Идентификатор массива по поведению эквивалентен постоянному указателю на объект данного типа.

Например,

Int const *(cptr_a);

будет вести себя как а.

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

int a [ 4 ] = { 1, 2, 3, 4 } ;

int ptr_a = a; // теперь ptr_a указывает на выделенную память

Кроме этого, указатель – это переменная и его можно увеличивать или уменьшать:

ptr_a++; // теперь ptr_a указывает на a [ 1 ] : *ptr_a == 2

Программа, иллюстрирующая различные способы применения указателей при обращении к элементам массива.

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

// Иллюстрация различных способов обращение к элементам массива на примере вычисления

// суммы пяти элементов вектора

#include <iostream>

using namespace std;

Int main()

{

int a[ ] = { 1, 2, 3, 4, 5 }; // определяем массив а

int *ptr_a = a; // инициализируем указатель ptr_a значением адреса первого элемента

// массива а ( адресом элемента a[0] )

// Данный оператор компилятор по умолчанию воспринимает как:

// int * const ptr_a = a;

int s = 0;

for(int i=0; i<4; i++)

s += a[i];

cout << "s = " << s; // результат 10

s = 0;

for(i=0; i<4; i++)

s += *(ptr_a+i); // помним, что ptr_a указывает на a[0], а

// ptr_a + 1 указывает на элемент a[1] и т.д.

cout << "\ns = " << s; // результат 10

s = 0;

for(i=0; i<4; i++)

s += *(a+i); // помним, что имя массива (а) является указателем

// на первый элемент массива

cout << "\ns = " << s; // результат 10

s = 0;

for(i=0; i<4; i++)

s += ptr_a[i]; // это "указатель-индекс". Указатель можно индексировать

// точно также, как и массив

cout << "\ns = " << s; // результат 10

getchar();

return 0;

}

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

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

Время жизни динамических переменных — от точки создания до конца программы или до явного освобождения памяти.

В С++ используется два способа работы с динамической памятью. Первый использует семейство функций malloc и достался в наследство от С, второй использует операции new и delete.

Существуют следующие способы инициализации указателя:

1. Присваивание указателю адреса существующего объекта:

  • с помощью операции получения адреса:

int a = 5;//целая переменная

int* p = &a;//в указатель записывается адрес a

int* p (&a);//то же самое другим способом

  • значения другого инициализированного указателя:

int* r = p;

  • имени массива или функции, которые трактуются как адрес:

int b[10];//массив

int* t = b;// Присваивание имени массива

...

void f(int a ){ /* … */ }// Определение функции

void (*pf)(int);// Указатель на функцию

pf = f;// Присваивание имени функции

2. Присваивание указателю адреса области памяти в явном виде:

char* vp = (char *)0xB8000000;//шестнадцатиричная константа

3. Присваивание пустого значения:

int* suxx = NULL;

int* rulez = 0;

4. Выделение участка динамической памяти и присваивание ее адреса указателю:

  • с помощью операции new:

int* n = new int;// 1

int* m = new int (10);// 2

int* q = new int [10];// 3

  • с помощью функции malloc:

int* u = (int*)malloc(sizeof(int));// 4

Освобождение памяти, выделенной с помощью операции new, должно выполняться с помощью delete, а памяти, выделенной функцией malloc — посредством функции free. При этом переменная-указатель сохраняется и может инициализироваться повторно. Приведенные выше динамические переменные уничтожаются следующим образом:

delete n; delete m; delete [] q; free (u);

ВНИМАНИЕ

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

Особенности объявлений

Обозначения, используемые для производных типов, достаточно трудны для понимания лишь потому, что операции * и & являются префиксными, а [] и () - постфиксными. Поэтому в задании типов, если приоритеты операций не отвечают цели, надо ставить скобки. Например, приоритет операции [] выше, чем у *, и мы имеем:

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]