
Урок 12-13.
План занятия.
Указатели
Выделение памяти с помощью операции new
Указатели и массивы
Указатели – аргументы функций. Передача аргументов по указателю.
Ссылки
Указатели.
Статическая память – это область хранения всех глобальных и статических переменных. Переменные статической памяти объявляются лишь единожды и уничтожаются по завершении программы.
Динамическая память или память свободного хранения отличается от статической тем, что программа должна явным образом запросить память для элементов, хранимых в этой области, а затем освободить память, если она больше не нужна. При работе с динамической памятью программа может позволить себе, например, узнать количество элементов массива на этапе выполнения.
Автоматическое, статическое и динамическое хранилище
В C++ предлагаются три способа управления памятью для данных — в зависимости от метода ее выделения: автоматическое хранилище, статическое хранилище и динамическое хранилище, иногда называемое свободным хранилищем или кучей. Объекты данных, выделенные этими тремя способами, отличаются друг от друга тем, насколько долго они существуют. Рассмотрим кратко каждый из них.
Автоматическое хранилище
Обычные переменные, объявленные внутри функции, используют автоматическое хранилище и называются автоматическими переменными. Этот термин означает, что они создаются автоматически при вызове содержащей их функции и уничтожаются при ее завершении.
Автоматические переменные обычно хранятся в стеке. Это значит, что когда выполнение программы входит в блок кода, его переменные последовательно добавляются к стеку в памяти и затем освобождаются в обратном порядке, когда выполнение покидает данный блок. (Этот процесс называется UFO (last-in, first-out — "последним пришел — первым ушел".) Таким образом, по мере продвижения выполнения стек растет и уменьшается.
Статическое хранилище
Статическое хранилище — это хранилище, которое существует в течение всего времени выполнения программы. Доступны два способа для того, чтобы сделать переменные статическими. Один заключается в объявлении их вне функций. Другой предполагает использование при объявлении переменной ключевого слова static:
static double fee = 56.50;
Автоматическое и статическое хранилища — строго определяют время жизни переменных. Переменные могут либо существовать на протяжении всего выполнения программы (статические переменные), либо только в период выполнения функции или блока (автоматические переменные).
Динамическое хранилище
Операции new и delete предлагают более гибкий подход, нежели использование автоматических и статических переменных. Они управляют пулом памяти, который в C++ называется свободным хранилищем или кучей. Этот пул отделен от области памяти, используемой статическими и автоматическими переменными.
Совместное применение new и delete предоставляет возможность более тонко управлять использованием памяти, чем в случае обычных переменных. Однако управление памятью становится более сложным.
Указатели представляют собой переменные, которые хранят адреса значений вместо самих значений.
Чтобы явно получить адрес обычной переменной, для этого применяется операция взятия адреса, обозначаемая символом &, к переменной, адрес которой интересует. Например, если home — переменная, то &home — ее адрес.
Использование операции & для нахождения адреса: address_find.cpp
#include <iostream>
using namespace std;
void main ()
{
setlocale (LC_CTYPE, "russian");
int a=6;
double b=2.35;
cout << "значение а = " << a << endl;
cout << "адрес а: " << &a << endl;
cout << "значение b = " << b << endl;
cout << "адрес b: " << &b << endl;
cin.get();
cin.get();
}
Указатель – специальный тип переменной, который может хранить адрес значения. Таким образом, имя указателя представляет местоположение. Применяя операцию *, называемую косвенным значением или операцией разыменования, можно получить значение, хранящееся в указанном месте.
Предположим, например, что manly — это указатель. В таком случае manly представляет адрес, a *manly — значение, находящееся по этому адресу. Комбинация *manly становится эквивалентом простой переменной типа int.
Первая переменная – указатель: pointer.cpp
#include <iostream>
using namespace std;
void main()
{
setlocale (LC_CTYPE, "russian");
int a=5; //объявление переменной
int *ptr; //объявление указателя на int (это значение типа int)
ptr=&a; //присвоить адрес int указателю
//выразить значение двумя способами
cout << "Величина а= " << a;
cout << " , *ptr= " << *ptr << endl;
//выразить адрес двумя способами
cout << "Адрес а: " << &a;
cout << " , ptr: " << ptr << endl;
//Изменить значение через указатель
*ptr +=1;
cout << "Новое значение а= " << *ptr << endl;
cin.get();
cin.get();
}
Таким образом:
а = *ptr (значение переменной, разыменованый указатель)
&a = ptr (адрес переменной, он же указатель)
int jumbo = 23
int *pe = &jumbo;
jumbo &jumbo
*pe одно и то же pe
--------------- -----------------
Значение:23 Адрес: 0х2ас8
Объявление и инициализация указателей
Следующий пример содержит объявление:
int * p_updates;
Мы говорим, что p_update указывает на тип int. Мы также говорим, что тип p_updates — это указатель на int, или точнее, int *. Итак, повторим еще раз: p_updates — это указатель (адрес), a *p_updates — это
int, а не указатель. К слову, пробелы вокруг операции * не обязательны. Традиционно программисты на С используют следующую форму:
int *ptr;
Тот же самый синтаксис применяется для объявления указателей на другие типы:
double * tax_ptr; // tax_ptr указывает на тип double
char * str; // str указывает на тип char
Инициализировать указатель можно в операторе объявления. В этом случае инициализируется указатель, а не значение, на которое он указывает. То есть следующие операторы устанавливают pt, а не *pt равным значению &higgens:
int higgens = 5;
int * pt = &higgens;
вместо:
int higgens = 5;
int * pt;
pt = &higgens;
Опасность, связанная с указателями
Опасность подстерегает тех, кто использует указатели неосмотрительно. Очень важно понять, что при создании указателя в коде C++ компьютер выделяет память для хранения адреса, но не выделяет памяти для хранения данных, на которые указывает этот адрес. Выделение места для данных требует отдельного шага. Если пропустить этот шаг, как в следующем фрагменте, то это обеспечит прямой путь к проблемам:
long * fellow; // создать указатель на long
*fellow = 223323; // поместить значение в неизвестное место
Конечно, fellow — это указатель. Но на что он указывает? Никакого адреса переменной fellow в коде не присвоено. Так куда будет помещено значение 223323? Ответить на это невозможно. Поскольку переменная fellow не была инициализирована, она может иметь какое угодно значение.
Внимание!
Золотое правило указателей: всегда инициализируйте указатель, чтобы определить точный
и правильный адрес, прежде чем применять к нему операцию разыменования (*).
Указатели и числа
Указатели — это не целочисленные типы, даже несмотря на то, что компьютеры обычно выражают адреса целыми числами. Целочисленные значения можно суммировать, вычитать, умножать, делить и т.д. Но указатели описывают местоположение, и не имеет смысла, например, перемножать между собой два местоположения. В терминах допустимых над ними операций указатели и целочисленные типы отличаются друг от друга. Следовательно, нельзя просто присвоить целочисленное значение указателю:
int * pt;
pt = 0хВ8000000; // несоответствие типов
Если вы хотите использовать числовое значение в качестве адреса, то должны выполнить приведение типа, чтобы преобразовать числовое значение к соответствующему типу адреса:
int * pt;
pt = (int *) 0xB8000000; // теперь типы соответствуют
____________________________________________________________
*ptr – значение
ptr – сам указатель, таким образом показывается адрес значения
&ptr – адрес указателя
Указатель - это переменная, содержащая адрес другой переменной. Указатели очень широко используются в языке C. Это происходит отчасти потому, что иногда они дают единственную возможность выразить нужное действие, а отчасти потому, что они обычно ведут к более компактным и эффективным программам, чем те, которые могут быть получены другими способами.
Так как указатель содержит адрес объекта, это дает возможность "косвенного" доступа к этому объекту через указатель. Предположим, что х - переменная, например, типа int, а рх - указатель, созданный неким еще не оговоренным способом. Унарная операция & (операция взятия адреса) выдает адрес объекта, так что оператор
pxх = &x; |
присваивает адрес x переменной px; говорят, что px "указывает" на x.
Операция & применима только к переменным и элементам массива, конструкции вида:
&(х-1) и &3 |
являются незаконными.
Унарная операция * (операция разыменования) рассматривает свой операнд как адрес конечной цели и обращается по этому адресу, чтобы извлечь содержимое. Следовательно, если y тоже имеет тип int, то
y = *px; |
присваивает y содержимое того, на что указывает px. Так последовательность
px = &x; y = *px; |
присваивает y то же самое значение, что и оператор
y = x; |
Переменные, участвующие во всем этом необходимо объявить, а для этого необходимо выяснить, каков же синтаксис создания указателя:
тип_данных * имя_указателя; |
Прокомментируем:
Тип_данных - любой существующий в программе тип. Должен СТРОГО!!! совпадать с типом той переменной, чей адрес будет храниться в указателе. Это связано с тем, что в момент разыменования операционной системе необходимо знать не только адрес объекта, но и его точный тип данных для правильности использования пространства по этому адресу. Отклонение от этого правила приведёт ошибке на этапе компиляции.
Примечание: Кстати! Размер, занимаемый указателем в оперативной памяти не зависит от его типа. Размер указателя определяется разрядностью операционной системы и тем, сколько байт требуется для хранения адреса. Например, в 32-разрядной системе это будет 4 байта, в 64-разрядной - 8 байт.
Из этого выводим объявление, использованных выше переменных:
int x, y; int *px; |
C описанием для x и y мы уже неодонократно встречались. Описание указателя
int *px; |
является новым и должно рассматриваться как мнемоническое; оно говорит, что комбинация *px имеет тип int. Это означает, что если px появляется в контексте *px, то это эквивалентно переменной типа int. Фактически синтаксис описания переменной имитирует синтаксис выражений, в которых эта переменная может появляться. Это замечание полезно во всех случаях, связанных со сложными описаниями. Например,
double A, *dp; |
говорит, что A и *dp имеют в выражениях значения типа double.
Указатели могут входить в выражения. Например, если px указывает на целое x, то *px может появляться в любом контексте, где может встретиться x. Например:
y = *px + 1; //присваивает y значение, на 1 большее значения x;
cout<< *px;//выводит текущее значение x;
d = sqrt((double) *px)//получает в d квадратный корень из x, причем до передачи функции sqrt значение x преобразуется к типу double. |
В выражениях вида
y = *px + 1; |
унарные операции * и & связаны со своим операндом более крепко, чем арифметические операции, так что такое выражение берет то значение, на которое указывает рх, прибавляет 1 и присваивает результат переменной y.
Разыменование указателя может располагаться и в левой части присваиваний. Если px указывает на x, то
*px = 0; |
полагает х равным нулю, а
*px += 1; |
увеличивает его на единицу, как и выражение (правда здесь изменения самой переменной x не происходит)
(*px) + 1; |
Круглые скобки в последнем примере необходимы; если их опустить, то поскольку унарные операции, подобные * и ++, выполняются справа налево, это выражение увеличит px, а не ту переменную, на которую он указывает.
И наконец, так как указатели являются переменными, то с ними можно обращаться, как и с остальными переменными. Если py - другой указатель на переменную типа int, то
py = px; |
копирует содержимое рх в py, в результате чего py указывает на то же, что и px. Напоминаем, что для данной процедуры указатели должны совпадать по типу данных!!!
Итак, мы познакомились с основами теории указателей. Следующий раздел урока расскажет вам о том, какая связь прослеживается между нашей сегодняшней темой и массивами