Out.close();//закрытие потока 2 глава




int (*pointer1), x1;

double (* pointer2), x2;

char (*pointer3);

 

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

Первый указатель рассмотренного примера, pointer1, предназначен для работы с переменными типа int, второй с переменными типа double, третий с переменными типа char. Существует более общий тип указателей с именем типом void *. Указатель типа void * отличается от указателей других типов тем, что для него не указывается размер соответствующего ему участка памяти.

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

pointer1=&x1;

pointer2=&x2;

означает, что указателю pointer1 следует присвоить адрес переменной x1, указателю pointer2 адрес x2. Теперь нам известны адреса x1 и x2, но как с ними работать? Например, как изменить в ячейках с этими адресами значения переменных. Ответ прост. Для этого есть специальный операторы – звездочка ' (* '). Звездочка это оператор, который читается так – значение переменной на которую указывает … далее имя указателя. Например,

*pointer1=2; /* значение переменной на которую указывает

* указатель pointer1 присвоить число равное 2 */

*pointer2=3;

Это все равно, что написать

x1=2;

x2=3;

Теперь к переменным x1 и x2 можно обращаться не только через имена, но и через указатель.

Оператор звездочка, с помощью которого переменной x1 было присвоено новое значение, называется оператором разыменовывания или косвенной адресации, а переменная-указатель разыменованой. По-существу разыменовывание означает для переменной получение второго имени, а не потерю первого, как это могло показаться. В рассмотренном примере x1 и *pointer1 это одна и та же переменная. То же самое можно сказать о x2 и *pointer2.

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

#include <iostream>

using namespace std;

int main(){

int *pointer1, x1,x2;

x1=1;

 

pointer1 =&x1; //указателю присвоить адрес x1

*pointer1 =2; /*переменной на которую указывает pointer1 присвоить

значение равное 2 */

x2=3*(*pointer1+5); /* разыменнованная переменная может

быть операндом */

cout<< "\n"<<x1; //Вывести x1

cout<<"\n"<< *pointer1; //Вывести x1, но с помощью указателя

cout<<"\n"<<x2; //Вывести x2

cout<< "\n"<<pointer1; //вывести адрес записанный в указатель

return 0;

}

на экране появится

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

Указатели можно инициализировать при их объявлении, например:

int ir;

int *pir=&ir //здесь инициализирован указатель, но не переменная

Как и любая переменная, указатель хранится в памяти, т.е. имеет адрес и значение. Адрес и значение такой переменной определяются с помощью все тех же унарных операций & и *. Переменная типа указатель занимает в памяти 2 байта.

Говоря об указателях, все времявремя встречается слово переменная. Это не значит, что нельзя применять указатели к константам. Вот пример объявления указателя для константы:

string str="Hello!";

string *const pstr=&str;

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

 

char const *const name;

Рассмотрим еще один пример иллюстрирующий возможности указателей.

#include<iostream>

#include<string>

using namespace std;

void main(){

char *psz="Dog Motja"; /*В указателе будет храниться адрес

первого символа строки */

for(int i=strlen(psz)-1;i>=0;i--) /*функция strlen вычисляет длину

строки, описана в string. Длина строки в этом примере (string)=9 в этом примере.

Нумеруются элементы массива от 0 т.е. в этом случае от 0 до 8*/

cout<<psz[i];

cout<<endl<<psz;

int z;

cin>> z;

}

 

А вот результат

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

Рассмотрим случай неправильного использования оператора &.

pic=&48; //число 48 не связано ни с одной ячейкой памяти

int ir=5;

pir=&(ir+15); /*арифметическое выражение не связано

с ячейкой памяти */

 

Поскольку указатели позволяют работать с памятью, то возникает вопрос, а нельзя ли в память сразу загружать значения переменных, минуя их объявление. Ответ – можно, ведь переменная, и её указатель, это одно и тоже. Делается это с помощью оператора new, например, нужно ввести новую переменную с помощью определенного заранее указателя. Фрагмент кода выглядит следующим образом

int *point; /* объявили указатель для переменной типа int,

* т.е. предупредили компилятор, что в программе

* будут использованы указатели с перечисленными

* именами – в данном случае одно */

point=new int; /* просим для переменной типа int дать

* свободный адрес.В дальнейшем к этой

* переменной будем обоащаться через указатель */

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

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

#include <iostream>

using namespace std;

int main(){

int *point1, *point2; //объявление переменных типа

point1=new int; /*выделение места в памяти для переменной

*типа int. Администатор кучи выделит место и

* запишет адрес в point1, т.е. инициализирует

* указатель */

*point1=1; /* переменной на которую указывает point1

* присвоить значение равное 1

point2=point1; /*обратите внимание указателю присваивается

* значение другого указателя, подобно присвоению

* переменных a=b, в итоге оба указателя имеют

* одинаковый адрес */

cout<<"\n *point1="<<*point1;

cout<<"\n *point2="<<*point2;

*point1=2; //новое значение динамической переменной

cout<<"\n *point1="<<*point1;

cout<<"\n *point2="<<*point2;

point1=new int; //новое место в памяти для point1

*point1=3; //новое значение динамической переменной

cout<<"\n *point1="<<*point1;

cout<<"\n *point2="<<*point2;

return 0;

}

В результате выполнения программы на экране должно появиться следующее.

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

При объявлении указателей приходится несколько раз пользоваться звездочкой, например

int * p1,*p2,*p3,*p4;

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

typedef int* point; // создать новый тип указателей с именем point

point p1,p2,p3,p4; //объявление переменных типа point

Первую строку можно прочитать так. Объявляется новый тип указателя для переменных типа int.Далее можно читать так - объявляются переменные типа point, эти переменные имеют имена p1,p2,p3,p4. Две строки рассмотренного кода эквивалентны одной, содержащей звездочки.

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

delete point1;

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

 

 

5.1.Типы указателей и операции с указателями

Указатели о которых мы говорили выше это, так называемые, указатели-переменные. Наряду с ними существуют указатели-константы. А вообще указатели делятся на две категории – указатели на объекты и указатели на функции. Здесь встретилось новое слово объект. Пока под объектом мы будем понимать все то, что не функция, т.е. это переменные любого типа, массивы и структуры. Позднее мы узнаем, что множество объектов значительно богаче. Определение указателей, как отмечалось, может быть совмещено с их инициализацией так

тип *имя_указателя = выражение;

тип *имя_указателя (выражение);

 

Пример

int x=1,y=2;

int * p1=&x, *p2=&y;

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

- разыменовывание или доступ по адресу *;

- преобразование типов;

- присваивание;

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

- сложение и вычитание;

- инкремент ++;

- декремент --;

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

С некоторыми из них мы уже знакомы. Другие не на много сложнее. Рассмотрим их.

Начнем с вычитания. Вычитание применимо к указателям на объекты одного типа. Выитание указателей одного типа позволяет определить расстояние меду двумя участками, причем за единицу памяти принимается размер памяти занимаемый этим объектом. Например, есть две переменные типа double. Каждая из них занимает 8 байтов. Даже если они находятся рядом, результатом вычитания указателей будет 1. Если между ними находится еще одна переменная того же типа, то разность будет равна 2. То есть вычитание указателей не совпадает с операцией вычитания адресов.

Вот пример программы иллюстрирующей сказанное

#include <iostream>

#include <windows.h>

using namespace std;

void main(){

SetConsoleOutputCP(1251);

//=============================================

int i1=1,i2=2,*pi1,*pi2;

pi1=&i1; pi2=&i2;

cout<<"Значение указателей типа int"<<endl;

cout<<"pi1="<<pi1<<"\tpi2="<<pi2<<endl; /* «\t»-горизонтальная

табуляция */

cout<<"Разность указателей типа int"<<endl;

cout<<"pi1-pi2="<<pi1-pi2<<endl;

//=============================================

double d1=1,d2=2;

double *pd1=&d1, *pd2=&d2;

cout<<" Значение указателей типа double"<<endl;

cout<<"pd1="<<pd1<<"\tpd2="<<pd2<<endl;

cout<<" Разность указателей типа double"<<endl;

cout<<"pd1-pd2="<<pd1-pd2<<endl;

//=============================================

char ch1='a',ch2='b',*pch1,*pch2; //указатели объявлены как символы

pch1=&ch1; pch2=&ch2;

cout<<" Значение указателей типа char"<<endl;

cout<<"pch1="<<(void *)pch1<<"\tpch2="<<(void *)pch2<<endl;

cout<<" Разность указателей типа char"<<endl;

cout<<"pch1-pch2="<<pch1-pch2<<endl;

//=============================================;

bool b1=true,b2=false, *pb1=&b1,*pb2=&b2;

cout<<" Значение указателей типа bool"<<endl;

cout<<"pb1="<<pb1<<"\tpb2="<<pb2<<endl;

cout<<" Разность указателей типа bool"<<endl;

cout<<"pb1-pb2="<<pb1-pb2<<endl;

}

А вот результат

 

Действительно для всех базовых переменных разность указателей получилась равной 1.

 

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

А сейчас рассмотрим еще один пример, в котором используется функция sizeof(argument). Эта функция возвращает количество байт занятых в памяти типом данных или выражением, передаваемым функции. Если передается выражение, то оно не вычисляется. Операция sizeof обычно используется для определения необходимого объема памяти при размещении переменной операцией new. Также она бывает необходима для определения числа элементов массива при обработке в цикле. Если sizeof стоит перед типом, то тип заключается в круглые скобки. Если перед переменной, то круглые скобки необязательны. Например,

int i;

cout<<sizeof(int);

cout<<sizeof i;

 

Итак, вот этот пример:

#include <windows>

#include <iostream>

using namespace std;

void main()

{

SetConsoleOutputCP(1251);

cout<<"Результаты различных случаев применения sizeof.\n";

cout<<"char:"<<sizeof(char)<<endl;

cout<<"char&:"<<sizeof(char&)<<endl;

cout<<"char*:"<<sizeof(char*)<<endl;

cout<<"wchar_t:"<<sizeof(wchar_t)<<endl; /* wchar_t расширенный

тип char */

cout<<"bool*:"<<sizeof(bool)<<endl;

cout<<"short*:"<<sizeof(short)<<endl;

cout<<"int:"<<sizeof(int)<<endl;

cout<<"long:"<<sizeof(long)<<endl;

cout<<"double:"<<sizeof(double)<<endl;

cout<<"long double:"<<sizeof(long double)<<endl;

enum DIGIT{ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE}; // enum –это перечисление для тех кто не знает

cout<<"DIGIT:"<<sizeof(DIGIT)<<"Member:"<<sizeof(ZERO)<<endl;

char ca[10];

cout<<"char[10]:"<<sizeof(ca)<<"элементов:"<<sizeof(ca)/sizeof(ca[0])<<endl;

double da[10];

cout<<"double[10]:"<<sizeof(da)<<"элементов:"<<sizeof(da)/sizeof(da[0])<<endl;

struct cd{

char c;

double d;

} cda[10];

cout<<"struct cd[10]:"<<sizeof(cda)<<"элементов:"<<sizeof(cda)/sizeof(cda[0])<<endl;

char *cp=new char[10];

cout<<"new char[10]:"<<sizeof(cp)<<"*cp:"<<sizeof(*cp)<<endl;

int i;

cin>>i;

}

Вот результат

 

Адресная арифметика

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

p1++;

указатель p1 принимает значение 2004, а не 2001. То есть, при увеличении на 1 указатель p1 будет ссылаться на следующее целое число. Это же справедливо и для операции уменьшения. Например, если p1 равно 2000, то после выполнения оператора

p1--;

значение p1 будет равно 1996.

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

char *ch = (char *) 3000;int *i = (int *) 3000; +------+ch --->| 3000 |--. +------+ |<- ich+1 ->| 3001 |--' +------+ch+2 ->| 3002 |--. +------+ |<- i+1ch+3 ->| 3003 |--' +------+ch+4 ->| 3004 |--. +------+ |<- i+2ch+5 ->| 3005 |--' +------+ Память
Рис. Пример размещения в памяти переменных char (слева) и short (справа)

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

p1 = p1 + 12;

"передвигает" указатель p1 на 12 объектов в сторону увеличения адресов.

Кроме суммирования и вычитания указателя и целого, разрешена еще только одна операция адресной арифметики: можно вычитать два указателя. Благодаря этому можно определить количество объектов, расположенных между адресами, на которые указывают данные два указателя; правда, при этом считается, что тип объектов совпадает с базовым типом указателей. Все остальные арифметические операции запрещены. А именно: нельзя делить и умножать указатели, суммировать два указателя, выполнять над указателями побитовые операции, суммировать указатель со значениями, имеющими тип float или double и т.д.

Сравнение указателей

Стандартом С допускается сравнение двух указателей. Например, если объявлены два указателя р и q, то следующий оператор является правильным:

if(p < q) printf("p ссылается на меньший адрес, чем q\n");#include <iostream.h> main(void){ char *ch1,*ch2; char str1[]="Hello!",str2[]="Good bye!"; ch1= &str1[0]; ch2=&str2[0]; cout<<"ch1="<<&ch1<<endl<<"ch2="<<&ch2<<endl; if(str1<str2) cout<<str1<<endl; else cout<<str2<<endl;}

Как правило, сравнение указателей может оказаться полезным, только тогда, когда два указателя ссылаются на общий объект, например, на массив. В качестве примера рассмотрим программу с двумя стековыми функциями, предназначенными для записи и считывания целых чисел. Стек — это список, использующий систему доступа "первым вошел — последним вышел". Иногда стек сравнивают со стопкой тарелок на столе: первая, поставленная на стол, будет взята последней. Стеки часто используются в компиляторах, интерпретаторах, программах обработки крупноформатных таблиц и в других системных программах. Для создания стека необходимы две функции: push() и pop(). Функция push() заносит числа в стек, a pop() — извлекает их. В данном примере эти функции используются в main(). При вводе числа с клавиатуры, программа помещает его в стек. Если ввести 0, то число извлекается из стека. Программа завершает работу при вводе -1.

#include <stdio.h>#include <stdlib.h> #define SIZE 50 void push(int i);int pop(void); int *tos, *p1, stack[SIZE]; int main(void){ int value; tos = stack; /* tos ссылается на основание стека */ p1 = stack; /* инициализация p1 */ do { printf("Введите значение: "); scanf("%d", &value); if(value!= 0) push(value); else printf("значение на вершине равно %d\n", pop()); } while(value!= -1); return 0;} void push(int i){ p1++; if(p1 == (tos+SIZE)) { printf("Переполнение стека.\n"); exit(1); } *p1 = i;} int pop(void){ if(p1 == tos) { printf("Стек пуст.\n"); exit(1); } p1--; return *(p1+1);}

Стек хранится в массиве stack. Сначала указатели p1 и tos устанавливаются на первый элемент массива stack. В дальнейшем p1 ссылается на верхний элемент стека, a tos продолжает хранить адрес основания стека. После инициализации стека используются функции push() и pop(). Они выполняют запись в стек и считывание из него, проверяя каждый раз соблюдение границы стека. В функции push() проверяется, что указатель p1 не превышает верхней границы стека tos+SIZE. Это предотвращает переполнение стека. В функции pop() проверяется, что указатель p1 не выходит за нижнюю границу стека.

В операторе return функции pop() скобки необходимы потому, что без них оператор

return *p1+1;

вернул бы значение, расположенное по адресу p1, увеличенное на 1, а не значение по адресу p1+1.

Преобразование типа указателя

Указатель можно преобразовать к другому типу. Эти преобразования бывают двух видов: с использованием указателя типа void* и без его использования.

В языке С допускается присваивание указателя типа void* указателю любого другого типа (и наоборот) без явного преобразования типа указателя. Тип указателя void* используется, если тип объекта неизвестен. Например, использование типа void* в качестве параметра функции позволяет передавать в функцию указатель на объект любого типа, при этом сообщение об ошибке не генерируется. Также он полезен для ссылки на произвольный участок памяти, независимо от размещенных там объектов. Например, функция размещения malloc() (рассматривается далее) возвращает значение типа void*, что позволяет использовать ее для размещения в памяти объектов любого типа.

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

#include <iostream.h>#include <windows.h>int main(void){SetConsoleOutputCP(1251); double x = 100.1, y; int *p; /* В следующем операторе указателю на целое p (присваивается значение, ссылающееся на double. */ p = (int *) &x; /* Следующий оператор работает не так, как ожидается. */ y = *p; /* попытка присвоить y значение x через p */ /* Следующий оператор не выведет число 100.1. */ cout<<" Значение x равно: "<<y<<" Это не так!"; return 0;}

Обратите внимание на то, что операция приведения типов применяется в операторе присваивания адреса переменной х (он имеет тип double*) указателю p, тип которого int*. Преобразование типа выполнено корректно, однако программа работает не так, как ожидается (по крайней мере, в большинстве оболочек). Для разъяснения проблемы напомним, что переменная int занимает в памяти 4 байта, а double — 8 байтов. Указатель p объявлен как указатель на целую переменную (т.е. типа int), поэтому оператор присваивания

y = *р;

передаст переменной y только 4 байта информации, а не 8 байтов, необходимых для double. Несмотря на то, что p ссылается на объект double, оператор присваивания выполнит действие с объектом типа int, потому что p объявлен как указатель на int. Поэтому такое использование указателя p неправильное.

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

Указатель void

В C++ существует специальный тип указателя, который называется указателем на неопределённый тип. Для определения такого указателя вместо имени типа используется ключевое слово void в сочетании с описателем, перед которым располагается символ *.

void *UndefPoint;

С одной стороны, объявленная подобным образом переменная также является объектом определённого типа - типа указатель на объект неопределённого типа. Имя UndefPoint действительно ссылается на объект размером в 32 бита со структурой, которая позволяет сохранять адреса.

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

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

UndefPoint = 0xb8000000; // Такое присвоение недопустимо.

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

UndefPoint++; // Для типа void * нет такой операции…

Эта операция (как и любая другая для типа указатель на объект неопределённого типа) не определена. И для того, чтобы не разбираться со всеми операциями по отдельности, лучше пресечь подобные недоразумения "в корне", то есть на стадии присвоения значения.

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

int mmm = 10;

pUndefPointer = (int *)&mmm;



Поделиться:




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

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


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