Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ЛекцияУказатели.doc
Скачиваний:
19
Добавлен:
18.04.2015
Размер:
169.98 Кб
Скачать

Лекция «Указатели в языке программирования С++»

Язык Си отличается от других структурированных немашинных языков ши­роким использованием указателей. В определенной степени именно наличие в языке С++ указателей сделало его очень удобным для системного программи­рования.

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

В процессе компиляции исходной программы все имена переменных преобразуются в адреса ячеек памяти, в которых размещаются соответствующие значения данных. В командах машинной программы при этом стоят машинные адреса размещения значений переменных. Это прямая адресация: вызов значения по адресу в команде. Например, по оператору присваивания: k = j; на машинном уровне при этом происходит копирование значения из области ОП, отведенной переменной j, в область ОП, отведенную переменной к. Таким образом, при выполнении машинной программы реализуются операции над операндами - значениями переменных, расположенными по определенным адресам ОП. Имена переменных в командах на машинном уровне не используются, а только адреса, сформированные транслятором с использованием имен переменных. Но программист не имеет доступа к этим адресам, если он не использует указатели.

Переменные-указатели содержат в качестве своих значений адреса памяти. Обычно переменная содержит определенное значение. С другой стороны, указатель содержит адрес переменной, которая содержит определенное значение. В этом смысле имя переменной отсылает к значению прямо, а указатель — косвенно (рис.1). Ссылка на значение посредством указателя называется косвенной адресацией (indirection).

count

7

сount прямо ссылается на переменную со значением 7

countPtr

count

7

сountPtr косвенно ссылается на переменную со значением 7

Указатель - это переменная или константа стандартного типа данных хранения адреса переменной определенного типа. Тип адресуемой переменной может быть стандартный, перечислимый, структурный, в виде объединения и void. Указатель на тип void может адресовать значение любого типа. Размер памяти для самого указателя и формат хранимого адреса (содержимого указателя) зависят от компьютера и выбранной модели памяти. Константа NULL из стандартного файла stdio.h предназначена для инициализации указателей ну­левым (незанятым) значением: адреса.

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

Объявление указателей

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

тип [модификатор] * имя-указателя ;

где тип - имя типа переменной, адрес которой будет содержать переменная- указатель (на которую он будет указывать);

имя-указателя - идентификатор переменной типа указатель; * - определяет переменную типа указатель.

Модификатор необязателен и может иметь значения:

  • near - ближний, 16-битовый указатель (устанавливается и по умолча­нию); предназначен для адресации 64-килобайтового сегмента ОП;

  • far - дальний, 32-битовый указатель; содержит адрес сегмента и смеще­ние в нем; может адресовать ОП объемом до 1 Мб;

  • huge - огромный, аналогичен указателю типа far; но хранится в норма­лизованном формате, что гарантирует корректное выполнение над ним операций отношения; применяется к функциям и к указателям для спецификации того, что адрес функции или адресуемой переменной имеет тип huge.

Пример: int *countPtr, count;

объявляет переменную countPtr типа int * (т.е. указатель на целое число) и чита­ется, как *countPtr является указателем на целое число» или *countPtr указыва­ет на объект типа int». Однако переменная count объявлена как целое число, но не как указатель на целое число. Символ * в объявлении относится только к countPtr. Каждая переменная, объявляемая как указатель, должна иметь перед собой знак звездочки (*). Например, объявление

double *xPtr, *yPtr;

указывает, что и xPti и yPtr являются указателями на значения типа double.. Ис­пользование * подобным образом в объявлении показывает, что переменная объяв­ляется как указатель.

Указатели можно объявлять, чтобы указывать на объекты любого типа данных.

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

Пример: int *pi; //указатель-переменная на данные типа int

char *pc; // - указатель-переменная на данные типа char

float *pf; // - указатель-переменная, на данные типа float

int ml [5]; // - имя массива на 5 значений типа int;

// ml - указатель-константа

int * m2[10]; // m2 - имя массива на 10 значений типа 'указатель

// на значения типа int, m2 - указатель-константа.

int (*m3)[10] // - указатель на массив из 10 элементов типа int;

// m3 - указатель-константа

float *fl; // - указатель-переменная на данные типа float

float *f2(); // - имя указателя-константы на функцию

Указатели должны инициализироваться либо при своем объявлении, либо в операторе присваивания.

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

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

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

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

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

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

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

int* r = р;

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

char* vp = (char *)0xB8000000;

Здесь 0хВ8000000 — шестнадцатеричная константа, (char *) — операция приведения типа: константа преобразуется к типу «указатель на char».

3. Указатель может инициализироваться значением 0, NULL или адресом. Указатель со значениями 0 или NULL ни на что не указывает.

int*s=Null;

int rules=0;

Символическая константа NULL определяется в заголовочном файле <iostream> (и в нескольких заголовочных файлах стандартной библиотеки). Инициализация указателя значением NULL эквивалентна инициализации указателя значением.0, но в C++ 0 предпочтительнее. Если присваивается 0, то он преобразуется в указатель соответствующего типа. Значение 0 — единственное целое значение, которое можно прямо присваивать переменной-указателю без предварительного приведе­ния целого числа к типу указателя.

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

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

& - для получения адреса переменной;

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

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

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

Например: & i, *pi.

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

& имя-переменной - получение адреса, определяет адрес размещения значения переменной (reference - ссылку на переменную) определенного типа;

* имя-указателя - получение значения определенного типа по заданному адресу; определяет содержимое, размещенное по адресу, который содержится в указателе-переменной или в указателе-константе; это косвенная адресация ("снятие ссылки" или "разыменование").

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

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

Например: int i, *pi; ... //pi - переменная-указатель

pi = & i; // pi получает значение адреса i

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

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

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

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

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

i = * pi; // i получает значение, расположенное по адресу,

// который содер­жится в указателе pi

Пример: если int у = 5;

int *yPtr; то yPtr=&y

присваивает адрес переменной у указателю yPtr. Говорят, что переменная yPtr «указывает» на у. Рисунок показывает схематическое представление памяти после выполнено указанное выше присваивание. На рисунке показана «связь указателя» с помощью стрелки от указателя к объекту, на который он указывает.

y

yPtr

5

На следующем рисунке показывает представление указателя в памяти в предположении, что целая переменная у хранится в ячейке 600000, а переменная указатель yPtr xpaнится в ячейке 500000. Операнд операции адресации должен быть lvalue (т.е. чем-то таким, чему можно присвоить значение так же, как переменной). Операция адресации не может быть применена к константам, к выражениям, не дающим результат, на который можно сослаться, и к переменный, объявленным с классом памяти register.

yPtr у

500000

600000

600000

5

Рис. 3. Представление у и yPtr в памяти

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

cout << *yPtr << endl;

печатает значение переменной у, а именно 5. Использование * указанным способом называется разыменованием указателя. Заметьте, что разыменованный указатель может также использоваться в левой части оператора присваивания, например,

*yPtr = 9;

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

cin>> *yPtr;

Разыменованный указатель является lvalue, или «левым выражением».

Пример: программа демонстрирует операции с указателями. Ячейки памяти выводятся как шестнадцатеричные целые с помощью <<.

int main(){

int a;

int *aPtr;

a=7;

aPtr=&a;

cout<<"Adress a "<<&a;

cout<<"\nZnachenie aPtr "<<aPtr;

cout<<"\n\nZnachenie a "<<a;

cout<<"\nZnachenie *aPtr "<<*aPtr;

cout<<"\nInversno\n&*aPtr="<<&*aPtr;

cout<<"\nZnachenie *&aPtr="<<*&aPtr<<endl;

getch();

return 0;}

Результат работы программы:

a 0x0012ff88

aPtr 0x0012ff88

a 7

*aPtr 7

&*aPtr=0x0012ff88

*&aPtr = 0x0012ff88

Пример: Схематично связь указателей pi и рс и значений переменных i и с дана на рисунке. Каждый указатель занимает 2 байта. Как и любая дру­гая, переменная типа указатель имеет адрес и значение. В примере на рис. адрес pi = 65520, адрес рс = 65518.

Как и любые переменные, переменная pi типа указатель имеет адрес и зна­чение. Операция & над переменной типа указатель: &pi - дает адрес местопо­ложения самого указателя, pi - имя указателя определяет его значение, a *pi - значение переменной, которую адресует указатель.

Все эти значения можно вывести, например, с помощью программы

#include <stdio.h>

#include <conio.h>

void main () {

char c = 'A';

int i = 7776;

int *pi = &i;

char *pc = &c;

printf ("pi = %u, *pi = %d, &pi = %u \n, pc = %u, *pc = %c, pc = %u\n",

pi, *pi, &pi, pc, *pc, &pc );

getch();}

В результате ее выполнения будет выведено:

pi = 65522, *pi = 7776, &pi = 65520

pc = 65525, *pc = A, &pc = 65518

Арифметические операции с указателями

Указатели могут применяться как операнды в арифметических выражениях, выражениях присваивания и выражениях сравнения. Однако не все операции разрешены применительно к переменным указателям. С указателями может выполняться ограниченное количество арифметических операций. Указатель можно увеличивать (++), уменьшать (--), складывать с указателем целые числа (+ или +=), вычитать из него целые числа (- или -=) или вычитать один указатель из другого.

Арифметические операции с указателями автоматически учитывают размер типа величин, адре­суемых указателями. Большинство современных компьютеров имеет 2-х или 4-х байтовые целые. Некоторые из более новых машин имеют 8-байтовые целые. Поскольку результат арифметических действий с указателями зависит от размера объектов, на которые указывает указатель, арифметические действия с указателями являются машинно-зависимыми.

В общепринятой арифметике сложение 3000+2 дает значение 3002. Это нормально, но не в случае арифметических действий с указателями. Когда целое складывается или вычитается из указателя, указатель не просто увеличивается или уменьшается на это целое, но это целое предварительно умножается на размер объекта, на который ссылается указатель. Количество байтов зависит от типа данных. Например, оператор vPtr + = 2; выработал бы значение 3008 (3000+2*4) в предположении, что целое хранится в 4 байтах памяти. Если целое хранится в двух байтах памяти, то предыдущие вычисления имели бы результатом в памяти ячейку 3004 (3000+2*2). Если бы был другой тип данных, то предыдущий оператор увеличивал бы указатель на количество байтов, вдвое превышающее число байтов, необходимых для хранения этого типа данных.

Например: значение типа char занимает 1 байт, типа int - 2 байта, а типа float - 4 байта. Добавление 1 к указателю добавит "квант памяти", т. е. количество байтов, которое занимает значение адресуемого типа. Для указателя на элементы массива это означает, что осуществляется переход к адресу следующего элемента массива, а не следующего байта. Т. е. значение указателя при переходе от элемента к элемент массива целых значений будет увеличиваться на 2, а типа float - на 4. Результат вычитания указателей определен в языке Си как значение типа int.

Инкремент перемещает указатель к следующему элементу массива, декремент — к предыдущему. Фактически значение указателя изменяется на величину sizeof(тип). Если указатель на определенный тип увеличивается или уменьшает­ся на константу, его значение изменяется на величину этой константы, умножен­ную на размер объекта данного типа.

Разность двух указателей — это разность их значений, деленная на размер типа в байтах (в применении к массивам разность указателей, например, на третий и шестой элементы равна 3). Суммирование двух указателей не допускается.

Пример: если vPtr содержит ячейку 3000, а v2Ptr содержит адрес 3008, оператор

x = v2Ptr – vPtr

присвоит x значение разности номеров элементов массива, на которые указывают vPtr и v2Ptr, в данном случае 2 Арифметика указателей теряет смысл, если она выполняется не над массивами. Мы не можем предполагать, что две переменные одинакового типа хранятся в памяти вплотную друг к другу, если они не соседствуют в массиве.

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

#include <stdio.h>

#include <conio.h>

main () {

int a[] = { 100, 200, 300 };

int *ptr1, *ptr2;

ptr1 = a; // - ptrl получает значение адреса а[0].

ptr2 = &a[2]; // - ptr2 получает значение адреса а[2]

ptr1++; // - увеличение значения ptrl на квант ОП: ptrl = &а[1]

ptr2++; // - увеличение значения ptr2 на квант ОП: ptr2 = &а[3]

printf (" ptr2 - ptrl = %d\n", ptr2 - ptr1);

getch();}

Результат выполнения операции вычитания определяет 2 кванта ОП для значений типа int:

ptr2 - ptrl = &a[3] - &а[1] = (а + 3) - (а + 1) = 2;

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

префиксные операции в последовательности справа налево;

использование значения, полученного после выполнения префиксных операций;

постфиксная операция над указателем.

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

Пример: s+= *рх++.

При его реализации сначала выполняется префиксная операция над указателем, т. е. определяется значение *рх — содержимое, расположенное по адресу рх; это значение используется для формирования текущего значения s. А затем выполняется постфиксная операция ++ - увеличения значения указателя на квант памяти, т. е. на 2 байта.

Пример: рассмотрим последовательность действий, за­данную в операторе

*р++ =10;

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

*р =10; р++;

Выражение (*р)++. напротив, инкрементирует значение, на которое ссылается указатель.

Указатель можно присваивать другому указателю, если оба указателя имеют одинаковый тип. В противном случае нужно использовать операцию приведения, чтобы преобразовать значение указателя в правой части присваивания к типу указателя в левой части присваивания. Исключением из этого правила является указатель на void (т.е. void*), который является общим указателем, способным представлять указатели любого типа. Указателю на void можно присваивать типы указателей без приведения типа. Однако указатель на void не может быть присвоен непосредственно указателю другого типа — указатель на void сначала должен быть приведен к типу соответствующего указателя.

Указатель void* не может быть разыменован. Например, компилятор знает, что указатель на int ссылается на четыре байта памяти на машине с 4-байтовыми целыми, но указатель на void просто содержит ячейку памяти для неизвестного типа данных — точное количество байтов, на которое ссылается указатель, неизвестно компилятору. Компилятор должен знать тип данных, чтобы определить количество байтов, которое должно быть разыменовано для определенного указателя. В случае указателя на void это количество байтов не может быть определено из типа.

Замечание: присваивание указателя одного типа указателю другого типа (отлично­го от void*) без приведения первого указателя к типу второго указателя вызывает синтаксическую ошибку. Разыменование указателя на void* является синтаксической ошибкой.

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