Programmirovanie_-_1_kurs / Методические указания к лабораторным работам 3-4
.pdfВ случае со структурой test использовалось выравнивание целого по четырехбайтной (словной) границе. В результате такого выравнивания и образовались "пустоты" в памяти.
Узнать, сколько места в памяти занимает тот или иной объект, можно при помощи оператора sizeof. Оператор sizeof может применяться как к переменным, так и к идентификаторам, обозначающим типы данных (в частности, к структурам):
struct test
{
char h; int b; double f;
};
test str;
int a1 = sizeof(str);
int a2 = sizeof(char) + sizeof(int) + sizeof(double);
cout << a1 << " " << a2; // "16 13"
Если для оператора sizeof указано имя массива, то оператор вернет размер всего массива, а не отдельного элемента.
int x[1000];
cout << sizeof(x); // 4000
3. УКАЗАТЕЛИ
3.1. Понятие адреса переменной
Любой объект программы, будь то обычная переменная базового или пользовательского типа, или массив, занимает в памяти определенную область. Местоположение объекта в памяти определяется его
адресом.
Чтобы узнать адрес конкретной переменной, используется унарная операция взятия адреса. Для
20
применения данной операции к объекту следует указать знак амперсанда перед его именем (&).
В следующем примере мы выведем на экран значения и адреса переменных a и b.
unsigned int a = 40000; unsigned int b = 300;
cout << "a (value): " << a << "\n"; cout << "a (address): " << &a << "\n"; cout << "b (value): " << b << "\n"; cout << "b (address): " << &b << "\n";
Возможный результат:
a (value): 40000
a (address): 0012FEAC b (value): 300
b (address): 0012FEA8
Как мы видим, адрес отображается в шестнадцатеричном виде и представляет собой некоторое четырехбайтное значение. Адрес показывает, где именно в памяти программы (с какого по счету байта памяти) размещается значение переменной.
Рисунок 5. Представление переменных a и b в памяти
3.2. Понятие указателя
Язык С/С++ позволяет осуществлять доступ к памяти при помощи специальных переменных, называемых
указателями.
21
Указатель – переменная, содержащая адрес некоторой ячейки памяти. Указатель определяет не только адрес, но и тип данных объекта, размещающегося в памяти по заданному адресу.
При объявлении указателя используется следующий синтаксис:
тип_данных* имя_указателя
От объявления обычной переменной данная конструкция отличается наличием символа «звездочка» (*), определяющего, что новый идентификатор будет являться указателем.
Под указатель, как и под любую другую переменную, отводится место в памяти, причем размер указателя не зависит от типа данных значения, на которое он ссылается. Компилятор выделяет под указатель либо два, либо четыре байта, в зависимости от используемой модели памяти.
Для записи определенного адреса в переменнуюуказатель используется оператор присваивания. Указатель, как и любая другая переменная, может быть проинициализирован сразу при своем объявлении.
//Объявляем символьную переменную Symbol char Symbol = 'Y';
//Объявляем указатель на переменную Symbol char* pSymbol = &Symbol;
//Объявляем целочисленную переменную Value
int Value = 1234;
//Объявляем указатель для ссылки на целочисленные переменные ...
int *pValue;
//... и присваиваем ему адрес переменной Value pValue = &Value;
Указатель может быть объявлен с типом данных void. Указатели void* используют в случае, когда тип данных объекта, на который будет ссылаться указатель, заранее
22
неизвестен. В дальнейшем, такой указатель должен быть явно приведен к какому-либо типу данных.
int a; double b; cin >> a;
cin >> b;
// ptr - указатель на нетипизированную
//область памяти void* ptr;
//В зависимости от результата проверки условия
//указатель ptr может ссылаться на значения
//различных типов данных
if (a>b) ptr = &a;
else
ptr = &b;
3.3. Операции с указателями
3.3.1. Разыменование указателя
К указателям может быть применена специальная
операция разыменования. Операция разыменования
позволяет обратиться к значению, размещающемуся в памяти по заданному адресу.
Для обозначения данной операции используется символ «звездочка» (*). «Звездочка» ставится перед указателем, через который выполняется обращение к значению в памяти.
Операция разыменования возвращает значение в соответствии с типом данных указателя. Следует отметить, что операцию разыменования нельзя применять к нетипизированным указателям void* без их приведения к определенному типу.
Операция разыменования может использоваться не только для чтения, но и для изменения данных, размещающихся в памяти.
23
Пример 1:
// Объявляем целую переменную Value1: int Value1 = 10;
//Объявляем указатель pValue1
//и присваиваем ему адрес переменной Value1: int* pValue1 = &Value1;
//Объявляем целую переменную Value2
//и присваиваем ей значение, хранящееся по
//адресу pValue1 (значение переменной Value1):
int Value2 = *pValue1;
cout << Value2; // 10
Пример 2:
double value = 0.01;
cout << *(&value); // 0.01
Пример 3:
//Используем указатель pX
//для изменения значения переменной X
int X = 10; int *pX = &X; *pX += 10;
cout << X; // 20
Пример 4:
//Указатель ptr типа данных void* используется
//для работы с разнотипными значениями
int |
a = 1; |
double |
b = 1.5; |
void* |
ptr; |
ptr = &a; |
// |
1 |
cout << *( (int*)ptr ); |
||
ptr = &b; |
|
|
*( (double*)ptr ) /= 5; |
// |
0.3 |
cout << b; |
24
3.3.2. Арифметические операции и операции сравнения
К указателям могут применяться некоторые арифметические операции и операции сравнения (таблица 1). Кроме того, к указателям допускается применять операторы инкремента и декремента.
Таблица 1. Операции с указателями
Операция |
Выражение |
|
Результат |
Описание |
|
||||
Равно (==) |
указатель1 == |
true/ |
Сравнивает два указателя на |
||||||
|
|
указатель2 |
|
false |
равенство адресов |
|
|||
Не |
равно |
указатель1 != |
true/ |
Сравнивает два указателя на |
|||||
(!=) |
|
указатель2 |
|
false |
неравенство адресов |
|
|||
Меньше |
указатель1 |
< |
true/ |
Возвращает |
|
истину, |
если |
||
(<) |
|
указатель2 |
|
false |
адрес указателя 1 меньше |
||||
|
|
|
|
|
адреса указателя 2 |
|
|||
Меньше |
указатель1 <= |
true/ |
Возвращает |
|
истину, |
если |
|||
или |
равно |
указатель2 |
|
false |
адрес указателя 1 меньше |
||||
(<=) |
|
|
|
|
или равен адресу указателя |
||||
|
|
|
|
2 |
|
|
|
||
Больше (>) |
указатель1 |
> |
true/ |
Возвращает |
|
истину, |
если |
||
|
|
указатель2 |
|
false |
адрес указателя 1 больше |
||||
|
|
|
|
|
адреса указателя 2 |
|
|||
Больше |
указатель1 >= |
true/ |
Возвращает |
|
истину, |
если |
|||
или |
равно |
указатель2 |
|
false |
адрес указателя 1 больше |
||||
(>=) |
|
|
|
|
или равен адресу указателя |
||||
|
|
|
|
2 |
|
|
|
||
Вычитание |
указатель1 |
– |
целое |
Вычисляет |
|
количество |
|||
(–) |
|
указатель2 |
|
число |
элементов |
заданного |
типа |
||
|
|
|
|
|
между указателями |
|
|||
Вычитание |
указатель |
– |
указатель |
Вычисляет |
|
указатель, |
|||
(–) |
|
целое_число |
|
|
отстоящий |
от |
заданного на |
||
Сложение |
указатель |
+ |
указатель |
определенное |
количество |
||||
элементов (в соответствии с |
|||||||||
(+) |
|
целое_число |
|
|
|||||
|
|
|
типом указателя) |
|
|||||
Инкремент |
указатель++ |
|
указатель |
Вычисляет |
|
указатель, |
|||
(++) |
|
++указатель |
|
|
отстоящий от заданного на 1 |
||||
Декремент |
указатель-- |
|
указатель |
элемент |
|
|
|
||
(--) |
|
--указатель |
|
|
|
|
|
|
25
Операции сравнения позволяют проверить равенство/неравенство адресов, содержащихся в указателях, а также определить взаимное расположение значений в памяти.
Пример 1:
//Убедимся, что указатели ptr1 и ptr2
//ссылаются на одну и ту же область памяти
int |
a |
= |
1; |
int* |
ptr1 = |
&a; |
|
int* ptr2 = |
&a; |
||
if (ptr1 == |
ptr2) |
cout << "Указатели ссылаются на одну переменную";
Пример 2:
//Убедимся, что указатели ptr1 и ptr2
//ссылаются на разные области памяти
int |
a |
= |
1; |
int |
b |
= |
1; |
int* |
ptr1 = |
&a; |
|
int* ptr2 = |
&b; |
||
if (ptr1 != |
ptr2) |
cout << "Указатели ссылаются на разные переменные";
Пример 3:
//Определим, в каком порядке
//в памяти размещаются переменные a и b
int |
a |
= |
1; |
int |
b |
= |
1; |
int* |
ptr1 = |
&a; |
|
int* |
ptr2 = |
&b; |
|
cout |
<< ptr1 << "\n"; |
cout << ptr2 << "\n"; if (ptr1 < ptr2)
cout << "ptr1 < ptr2";
26
В результате сложения указателя с целым числом N получается новый адрес, опережающий исходный на M
байт, причем: M = N*размер_типа_данных_указателя.
Например, при добавлении к указателю типа double* целого числа 2, мы сместимся в памяти на 16 байт (16 = 2*8, 8 – размер типа данных double). Аналогичным образом работает операция вычитания (целого числа) и операторы инкремента/декремента.
Использование арифметических операций, а также операторов инкремента/декремента позволяет перемещаться в памяти от одного элемента массива к другому, например:
double X[]={1.1, 2.5, 3.7, 4, 5}; double* ptr = &(X[0]);
cout << ptr << " " << *ptr << "\n"; ptr++;
cout << ptr << " " << *ptr << "\n"; ptr = ptr+2;
cout << ptr << " " << *ptr << "\n"; ptr = ptr-3;
cout << ptr << " " << *ptr << "\n";
Результаты:
002EF794 1.1 002EF79C 2.5 002EF7AC 4 002EF794 1.1
Операция вычитания указателя из указателя может применяться в том случае, если оба указателя имеют одинаковый тип данных. Результатом вычитания указателя из указателя является целое число N, определяющее количество элементов заданного типа, размещающихся в памяти между двумя адресами. Число N определяется как отношение количества байт, разделяющих в памяти два адреса, к размеру типа данных указателя:
27
int Y[10];
int* ptr1 = &(Y[2]); int* ptr2 = &(Y[5]); cout << ptr1 << endl; cout << ptr2 << endl; int x = ptr2 - ptr1; cout << x;
Результаты:
0012FE90
0012FE9C
3
3.4. Особенности применения указателей
3.4.1. Значение NULL
Указатель может содержать специальное значение NULL. Значение NULL соответствует нулевому адресу и говорит о том, что указатель не ссылается на какую-либо ячейку памяти (другими словами, ни на что не указывает).
Следует отметить, что попытка разыменовать нулевой указатель приведет к ошибке:
int* A = NULL;
cout << *A; // !!! Ошибка (Access violation) // Разыменовывать адрес NULL нельзя
3.4.2. Указатели и массивы
Имя объявляемого массива всегда ассоциируется компилятором с адресом его первого элемента. Таким образом, идентификатор, обозначающий имя массива, одновременно является и указателем.
Пример:
int A[6] = {0,1,2,3,4,5}; cout << A;
Возможный результат:
0012FE98
Разыменовывая идентификатор массива, мы получаем доступ к элементу с индексом 0:
28
int A[5] = {10,20,30,40,50}; cout << *A; // cout << A[0];
Прибавляя к указателю целые числа, можно перемещаться по объектам массива:
int A[5] = {10,20,30,40,50}; cout << *(A+2); // cout << A[2];
В следующем примере проинициализируем массив квадратами индексов его элементов, используя адрес первого элемента, а затем выведем все элементы на экран:
const int N=10; int A[N];
for (int i=0; i<N; i++)
{
int* ptr = A+i; *ptr = i*i;
}
for (int i=0; i<N; i++) cout << A[i] << " ";
Результат:
0 1 4 9 16 25 36 49 64 81
3.4.3. Применение к указателям оператора sizeof
Как и к любой другой переменной, к указателям можно применять оператор sizeof. Для явно объявленных указателей оператор sizeof вернет их размер (2 или 4 байта, в зависимости от принятой модели памяти).
Следует отметить, что для идентификатора, объявленного как массив, оператор sizeof вернет размер массива, а не указателя:
int X[] = {1, 2, 3};
cout << sizeof(X) << "\n"; // 12 int* ptr = X;
cout << sizeof(ptr) << "\n"; // 4
3.4.4. Указатели на указатели
Указатель может ссылаться на переменную, которая, в свою очередь, также является указателем. В таком случае,
29