Студопедия

Главная страница Случайная страница

Разделы сайта

АвтомобилиАстрономияБиологияГеографияДом и садДругие языкиДругоеИнформатикаИсторияКультураЛитератураЛогикаМатематикаМедицинаМеталлургияМеханикаОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРелигияРиторикаСоциологияСпортСтроительствоТехнологияТуризмФизикаФилософияФинансыХимияЧерчениеЭкологияЭкономикаЭлектроника






Ход работы. Основные сведения об указателях в языке C++.Когда компилятор обрабатывает оператор определения переменной






Основные сведения об указателях в языке C++. Когда компилятор обрабатывает оператор определения переменной, например, int i=10;, он выделяет память в соответствии с типом (int) и инициализирует ее указанным значением (10). Все обращения в программе к переменной по ее име­ни (i) заменяются компилятором на адрес области памяти, в которой хранится значение переменной. Программист может определить собственные переменные для хранения адресов областей памяти. Такие переменные называются указате­лями.

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

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

 

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

 

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

int (*fun) (double, double);

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

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

тип *имя;

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

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

int *а, b, *с;

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

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

Указатель на voidприменяется в тех случаях, когда конкретный тин объекта, ад­рес которого требуется хранить, не определен (например, если в одной и той же переменной в разные моменты времени требуется хранить адреса объектов раз­личных типов).

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

Указатель может быть константой или переменной, а также указывать на кон­станту или переменную. Рассмотрим примеры:

int i; // целая переменная

const int ci = 1; // целая константа

int * pi; // указатель на целую переменную

const int * pci: // указатель на целую константу

int * const cp = & i; // указатель-константа на

// целую переменную

const int * const cpc = & ci: // указатель

// константа на целую константу

 

Как видно из примеров, модификатор const, находящийся между именем указа­теля и звездочкой, относится к самому указателю и запрещает его изменение, a const слева от звездочки задает постоянство значения, на которое он указывает. Для инициализации указателей использована операция получения адреса &.

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

Инициализация указателей. Указатели чаще всего используют при работе с динамической памятью, называе­мой некоторыми эстетами кучей (перевод с английского языка слова heap). Это свободная память, в которой можно во время выполнения программы выделять место в соответствии с потребностями. Доступ к выделенным участкам динами­ческой памяти, называемым динамическими переменными, производится только через указатели. Время жизни динамических переменных - от точки создания до конца программы или до явного освобождения памяти. В C++ используется два способа работы с динамической памятью. Первый использует семейство функций mallос и достался в наследство от С, второй использует операции new и delete.

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

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

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

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

 

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

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

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

 

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

 

int* r = p;

1.3. с помощью имени массива или функции, которые трактуются как адрес:

 

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

int* t = b; // присваивание адреса начала массива

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

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

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

 

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

char* vp = (char *)0xB8000000;

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

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

 

int* suxx = NULL;

int* rulez = 0;

 

В первой строке используется константа NULL, определенная в некоторых за­головочных файлах С как указатель, равный нулю. Можно использо­вать просто 0, так как это значение типа int будет правильно преобразовано стандартными способами в соответствии с контекстом. Поскольку гарантиру­ется, что объектов с нулевым адресом нет, пустой указатель можно использо­вать для проверки, ссылается указатель на конкретный объект или нет.

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

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

 

int* n = new int; // 1

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

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

 

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

 

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

 

В операторе 1 операция new выполняет выделение достаточного для размещения величины типа int участка динамической памяти и записывает адрес начала это­го участка в переменную n. Память под саму переменную n (размера, достаточно­го для размещения указателя) выделяется на этапе компиляции.

В операторе 2, кроме описанных выше действий, производится инициализация выделенной динамической памяти значением 10.

В операторе 3 операция new выполняет выделение памяти под 10 величин типа int (массива из 10 элементов) и записывает адрес начала этого участка в пере­менную q, которая может трактоваться как имя массива. Через имя можно обра­щаться к любому элементу массива. Если память выделить не удалось, по стандарту должно порождаться исключе­ние bad_alloc. Старые версии компиляторов могут возвращать 0.

В операторе 4 делается то же самое, что и в операторе 1, но с помощью функции выделения памяти malloc, унаследованной из библиотеки С. В функцию переда­ется один параметр - количество выделяемой памяти в байтах. Конструкция (int*) используется для приведения типа указателя, возвращаемого функцией, к требуемому типу. Если память выде­лить не удалось, функция возвращает 0.

Операцию new использовать предпочтительнее, чем функцию malloc, особенно при работе с объектами.

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

 

delete n;

delete m;

delete [] q;

free (u);

 

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

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

int *(*р[10])();

объявляется массив из 10 указателей на функции без параметров, возвращающих указатели на int.

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

При интерпретации сложных описаний необходимо придерживаться правила «изнутри наружу»:

1) если справа от имени имеются квадратные скобки, это массив, если скобки круглые - это функция;

2) если слева есть звездочка, это указатель на проинтерпретированную ранее конструкцию;

3) если справа встречается закрывающая круглая скобка, необходимо приме­нить приведенные выше правила внутри скобок, а затем переходить наружу;

4) в последнюю очередь интерпретируется спецификатор типа.

Для приведенного выше описания порядок интерпретации указан цифрами:

 

int *(*р[10])();

5 4 2 1 3 // порядок интерпретации описания

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

Операция разадресации, или разыменования, предназначена для доступа к ве­личине, адрес которой хранится в указателе. Эту операцию можно использовать как для получения, так и для изменения значения величины (если она не объяв­лена как константа):

 

char a; // переменная типа char

char * р = new char; /* выделение памяти под указатель и под динамическую переменную типа char */

*р = 'Ю'; а = *р; /* присваивание значения обеим переменным*/

 

Как видно из примера, конструкцию *имя_указателя можно использовать в левой части оператора присваивания, так как она является L-значением, то есть определяет адрес области памяти. Для простоты эту конструкцию можно считать именем переменной, на которую ссылается указатель. С ней допустимы все действия, определенные для величин соответствующего типа (если указатель инициализирован). На одну и ту же область памяти может ссылаться несколько указателей различного типа. Примененная к ним сиерация разадресации даст разные результаты. Например, программа

 

#include < stdio.h>

int main()

{

unsigned long int A = 0Xcc77ffaa;

unsigned short int* pint = (unsigned short int*) & A;

unsigned char* pchar = (unsigned char *) & A;

printf(“ | %x | %x | %x |”, A, *pint, *pchar);

return 0;

}

 

на IBM PC-совместимом компьютере выведет па экран строку:

| cc77ffaa | ffaa | аа |

Значения указателей pint и pchar одинаковы, но разадресация pchar дает в резуль­тате один младший байт по этому адресу, a pint - два младших байта. В приведенном выше примере при инициализации указателей были использова­ны операции приведения типов. Синтаксис операции явного приведения типа прост: перед именем переменной в скобках указывается тип, к которому ее требу­ется преобразовать. При этом не гарантируется сохранение информации, поэто­му в общем случае явных преобразований типа следует избегать.

При смешивании в выражении указателей разных типов явное преобразование типов требуется для всех указателей, кроме void*. Указатель может неявно пре­образовываться в значение типа bool (например, в выражении условного опера­тора), при этом ненулевой указатель преобразуется в true, а нулевой в false. Присваивание без явного приведения типов допускается в двух случаях:

1. указателям типа void*;

2. если тип указателей справа и слева от операции присваивания один и тот же.

Таким образом, неявное преобразование выполняется только к типу void*. Зна­чение 0 неявно преобразуется к указателю на любой тип. Присваивание указате­лей на объекты указателям на функции (и наоборот) недопустимо. Запрещено и присваивать значения указателям-константам, впрочем, как и константам любо­го типа (присваивать значения указателям на константу и переменным, на кото­рые ссылается указатель-константа, допускается).

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

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

short * р = new short [S3;

р++; // значение р увеличивается на 2

long * q = new long [5];

q++; // значение q увеличивается на 4

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

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

*р++ = 10;

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

*р = 10; р++;

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

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

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

Обращение к элементу динамического массива осуществляется так же, как и к эле­менту обычного -­­­ например а[3]. Можно обратиться к элементу массива и другим способом - *(а+3). В этом случае мы явно задаем те же действия, что выполня­ются при обращении к элементу массива обычным образом. Рассмотрим их подробнее. В переменной-указателе а хранится адрес начала массива. Для получения адреса третьего элемента к этому адресу прибавляется смещение 3. Операция сло­жения с константой для указателей учитывает размер адресуемых элементов, то есть на самом деле индекс умножается на длину элемента массива: а + 3*sizeof(int). Затем с помощью операции * (разадресации) выполняется выборка значения из указанной области памяти.

Если динамический массив в какой-то момент работы программы перестает быть нужным и мы собираемся впоследствии использовать эту память повторно, необ­ходимо освободить ее с помощью операции delete[], например:

 

delete[] a;

 

Размерность массива при этом не указывается.

Таким образом, время жизни динамического массива, как и любой динамической переменной, - с момента выделения памяти до момента ее освобождения. Область действия зависит от места описания указателя, через который производится рабо­та с массивом. Область действия и время жизни указателей подчиняются общим правилам, рассмотренным на первом семинаре. Как вы помните, локальная пере­менная при выходе из блока, в котором она описана, «теряется». Если эта перемен­ная является указателем и в ней хранится адрес выделенной динамической памя­ти, при выходе из блока эта память перестает быть доступной, однако не помечается как свободная, поэтому не может быть использована в дальнейшем. Это называет­ся утечкой памяти и является распространенной ошибкой:

{ // пример утечки памяти

int n;

cin > > n;

int* pmas = new int[n];

}

// После выхода из блока указатель pmas недоступен

 

Пример 5.1. Написать программу, которая для вещественного массива из n элементов определяет сумму его элементов, расположенных правее последнего отрицательного элемента.

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

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

 






© 2023 :: MyLektsii.ru :: Мои Лекции
Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав.
Копирование текстов разрешено только с указанием индексируемой ссылки на источник.