Тема 4. Указатели. Адресная арифметика. Динамические массивы
Функция как производный тип. Указатель на функцию (начало)
Функция как производный тип. Указатель на функцию (продолжение)
Функция как производный тип. Указатель на функцию (окончание)
Указатели и массивы
3.75M
Category: programmingprogramming

Лекция №4. Указатели. Адресная арифметика. Динамические массивы

1. Тема 4. Указатели. Адресная арифметика. Динамические массивы

• Указатели. Адресная арифметика
• Динамические массивы

2.

Операции взятия адреса и разыменования
При выполнении инициализации переменной, ей автоматически
присваивается свободный адрес памяти, и, любое значение, которое
мы присваиваем переменной, сохраняется в этом адресе памяти.
Например: если для переменной short int b; выделен адрес 16100, то
всякий раз, когда программа встречает в выражении переменную b,
то она понимает, что для того, чтобы получить значение — ей нужно
заглянуть в ячейку памяти под номером 16100. Мы просто ссылаемся
на переменную через присвоенный ей идентификатор, а компилятор
конвертирует это имя в соответствующий адрес памяти.
2

3.

Операции взятия адреса и разыменования
Согласно принципу адресности фон Неймана память делится на
ячейки одинакового размера, порядковый номер ячейки
считается ее адресом. При этом одна ячейка памяти занимает - 1
байт.
Как узнать, какой именно адрес присвоен конкретной переменной
(другими словами – где именно хранится значение переменной)?
Операция взятия адреса (&) позволяет узнать, какой адрес
памяти присвоен определённой переменной.
3

4.

Операции взятия адреса и разыменования
#include <iostream>
using namespace std;
int main()
{
int a = 15;
cout << a << '\n'; // выводим
значение переменной a
cout << &a << '\n'; // выводим
адрес памяти переменной a
return 0;
}
15
00000028ECAFFA74
4

5.

Операции взятия адреса и разыменования
Хотя оператор адреса выглядит так же, как оператор побитового И,
отличить их можно следующим образом:
• оператор адреса является унарным оператором;
• оператор побитового И является бинарным оператором.
Зачем нам адрес переменной? Мы же знаем её имя и в процессе
написания программы можем обратиться к нашей переменной по
имени. Однако есть такие ситуации, когда нам требуется именно
адрес переменной. Например , если мы передали значение
переменной в другую функцию в качестве параметра по значению, то
в данной функции создастся только копия этой переменной,
5
изменения не отразятся в головной программе.

6.

Операции взятия адреса и разыменования
При передаче параметра по ссылке в функцию передается именно
адрес переменной, и изменения немедленно отразятся в головной
программе, которая тоже «знает» этот адрес.
Операция разыменования (*) позволяет получить значение по
указанному адресу
6

7.

Операции взятия адреса и разыменования
#include <iostream>
using namespace std;
int main()
{
int a = 15;
// выводим значение ПЕРЕМЕННОЙ a
cout << a << '\n';
cout << &a << '\n'; // выводим
АДРЕС a
// выводим значение ЯЧЕЙКИ
ПАМЯТИ по адресу,
// выделенному для переменной a
cout << *&a << '\n';
return 0;
}
15
000000ADA3CFFAC4
15
7

8.

Указатели
Указатель – это производный тип, который представляет собой
АДРЕС какого-либо значения.
Указатели объявляются точно так же, как и обычные переменные,
только со звёздочкой между типом данных и идентификатором:
int *iPtr; // указатель на значение типа int
double *dPtr; // указатель на значение типа double
int* iPtr3; // допустимо, но не желательно
int * iPtr4; // (НЕЛЬЗЯ: пробел между * и именем
недопустим)
int *iPtr5, *iPtr6;// объявляем два указателя для
переменных типа int
8

9.

Указатели
Синтаксически C++ принимает объявление указателя, когда
звёздочка находится рядом с типом данных, с идентификатором или
даже посередине.
Эта звёздочка не является оператором разыменования!
Это всего лишь часть синтаксиса объявления указателя.
При объявлении нескольких указателей,
находиться возле каждого идентификатора!
звёздочка
должна
9

10.

Указатели
Присваивание значений указателю
Поскольку указатели содержат только адреса, то при присваивании
указателю значения — это значение должно быть адресом.
#include <iostream>
using namespace std;
int main()
{
int val = 5;
// инициализируем ptr адресом значения
val
int* ptr = &val;
// выводим адрес значения переменной
val
cout << &val << '\n';
// выводим адрес, который хранит ptr
cout << ptr << '\n';
return 0;
}
00000059BA0FFB74
00000059BA0FFB74
10

11.

Указатели
Тип указателя должен соответствовать типу переменной, на которую
он указывает!
C++ не позволяет напрямую присваивать адреса памяти указателю
при его объявлении!
Оператор взятия адреса возвращает именно адрес (который можно
присвоить указателю), а не число!
11

12.

Указатели
int iVal = 7;
double dVal = 9.0;
int* iPtr = &iVal; // ок
double* dPtr = &dVal; // ок
iPtr = &dVal; // неправильно
dPtr = &iVal; // неправильно
int* ptr = 7; // неправильно
double* dPtr = 0x0012FF7C; // неправильно
12

13.

Указатели
#include <iostream>
using namespace std;
int main()
{
int val = 5;
cout << &val << endl; // выводим адрес
cout << val << endl; // выводим значение
int* ptr = &val; // ptr указывает на val
// выводим адрес, который хранится в ptr,
т.е. &val
cout << ptr << endl;
// разыменовываем ptr: получаем значение
cout << *ptr << endl;
return 0;
}
0000004FBD7BF6B4
5
0000004FBD7BF6B4
5
13

14.

Указатели
Одному указателю можно присваивать разные значения:
#include <iostream>
using namespace std;
int main()
{
int val1 = 15;
int val2 = 5;
int* ptr;
ptr = &val1; // ptr указывает на val1
cout << *ptr << endl; // выведется 15
ptr = &val2; // ptr теперь указ. на
val2
cout << *ptr << endl; // выведется 5
}
15
5
14

15.

Указатели
Поскольку *ptr обрабатывается так же, как и val, то мы можем
присваивать ему значения так, как если бы это была бы обычная
переменная:
#include <iostream>
using namespace std;
int main()
{
int val = 5;
int* ptr = &val; // ptr указывает на val
*ptr = 7;
// *ptr - это то же самое, что и val, которому мы присвоили знач
cout << val << endl; // выведется 7
15

16.

Указатели
int* xptr, x = 10;
xptr = &x;
int y = x; /* присвоить переменной y значение x */
cout << y << endl; // выведется 10
y = *xptr; /* присвоить переменной y значение, */
/* находящееся по адресу xptr */
cout << y << endl; // выведется 10
}
7
10
10
16

17.

Указатели
int* xptr, x = 10;
xptr = &x;
int y = x; /* присвоить переменной y значение x */
cout << y << endl; // выведется 10
y = *xptr; /* присвоить переменной y значение, */
/* находящееся по адресу xptr */
cout << y << endl; // выведется 10
}
7
10
10
17

18.

Адресная арифметика
Указатель – это не просто адрес, а адрес величины определенного
типа.
С указателями можно выполнять не только операции присваивания и
обращения по адресу, но и ряд арифметических операций:
• указатели одного и того же типа можно сравнивать с помощью
стандартных операций сравнения. При этом сравниваются значения
указателей, а не значения величин, на которые данные указатели
ссылаются.
• к указателю можно прибавить целое число или вычесть из него
целое число. Результатом прибавления к указателю единицы
является адрес следующей величины типа, на который ссылается
указатель, в памяти.
18

19.

Адресная арифметика
Пусть xPtr – указатель на целое число типа long, а cp – указатель на
тип char. Начиная с адреса 1000, в памяти расположены два целых
числа. Адрес второго — 1004 (в большинстве реализаций Си++ под
тип long выделяется четыре байта). Начиная с адреса 2000, в памяти
расположены объекты типа char.
19

20.

Адресная арифметика
Размер памяти, выделяемой для числа типа long и для char, различен.
Поэтому адрес при увеличении xPtr и cp тоже изменяется по-разному.
Однако и в том, и в другом случае увеличение указателя на единицу
означает переход к следующей в памяти величине того же типа.
Прибавление или вычитание любого целого числа работает по тому
же принципу, что и увеличение на единицу. Указатель сдвигается
вперед (при прибавлении положительного числа) или назад (при
вычитании положительного числа) на соответствующее количество
объектов того типа, на который показывает указатель. Вообще говоря,
неважно, объекты какого типа на самом деле находятся в памяти —
адрес просто увеличивается или уменьшается на необходимую
величину. На самом деле значение указателя ptr всегда изменяется на
число, кратное sizeof(*ptr).
20

21.

Адресная арифметика
• указатели одного и того же типа можно друг из друга вычитать.
Разность
указателей
показывает,
сколько
объектов
соответствующего типа может поместиться между указанными
адресами.
#include <iostream>
using namespace std;
int main()
{
int x = 10; int y = 10;
int* xptr = &x; int* yptr = &y;
cout << xptr << endl;
cout << yptr << endl;
cout << (xptr == yptr) << endl;
cout << (*xptr == *yptr) << endl;
}
00000035698FF974
00000035698FF994
0
1
21

22.

Адресная арифметика
В языке C определена символическая константа NULL для
обозначения нулевого значения указателя. Такое использование
нулевого указателя было основано на том, что по адресу 0 данные
программы располагаться не могут, он зарезервирован операционной
системой для своих нужд. Однако во многом нулевой указатель –
просто удобное соглашение, которого все придерживаются.
При работе с указателями надо отличать операции с самим
указателем и операции со значением по адресу, на который
указывает указатель.
22

23.

Адресная арифметика
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int* pa = &a;
int b = *pa + 20; // операция со значением, на
который указывает указатель
pa++; // операция с самим указателем
cout << "b: " << b << endl; ; // 30
}
23

24.

Адресная арифметика
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int* pa = &a;
cout << "pa: address=" << pa << " val=" << *pa << endl;
int b = *pa++; // инкремент адреса указателя
cout << "b: val=" << b << endl;
cout << "pa: address=" << pa << " val=" << *pa << endl;
}
pa: address=000000AB81D6F5E4 val=10
b: val=10
pa: address=000000AB81D6F5E8 val=-858993460
24

25.

Выделение памяти с помощью операции new
Истинная ценность указателей проявляется тогда, когда во время
выполнения выделяются неименаванные области памяти для
хранения переменных. В этом случае указатели становятся
единственным способом доступа к такой памяти.
int * pn = new int;
Часть new int сообщает программе, что вам требуется некоторое новое
хранилище, подходящее для хранения int. Операция new использует
тип для того, чтобы определить, сколько байт необходимо выделить.
Затем она находит память и возвращает адрес. Далее вы присваиваете
адрес переменной pn, которая объявлена как указатель на int. Теперь
25
pn - адрес, а *pn - значение, хранящееся по этому адресу.

26.

Выделение памяти с помощью операции new
26

27.

Выделение памяти с помощью операции new
27

28.

Нехватка памяти
Если у компьютера не окажется достаточно доступной памяти,
чтобы удовлетворить запрос new, то new возвращает значение 0.
В С++ указатель со значением 0 называется null-указателем (нулевым
указателем). С++ гарантирует, что нулевой указатель никогда не
указывает на корректные данные, поэтому он часто используется в
качестве признака неудачного завершения операций или функций,
которые в противном случае должны возвращать корректные
указатели.
При помощи оператора if вы можете проверять возвращаемое
значение new на равенство нулевому указателю, и таким образом
защитить вашу программу от попыток выйти за границы памяти.
В дополнение к возврату нулевого указателя при сбое выделения
памяти, new может также возбуждать исключение bad_alloc. 28

29.

Освобождение памяти с помощью операции delete
Операция delete позволяет вам вернуть память в пул свободной
памяти, когда вы завершили работу с ней. Это - важный шаг к наиболее
эффективному использованию памяти.
Память, которую вы возвращаете, или освобождаете, затем может
быть повторно использована другими частями программы. Вы
используете delete, задавая после него указатель на блок памяти,
который был выделен операцией new:
int *ps = new int;
/ / выделить память операцией new
...
/ / использовать память
delete ps;
/ / освободить память операцией delete, когда она
больше не нужна
29

30.

Освобождение памяти с помощью операции delete
Это освобождает память, на которую указывает ps, но не удаляет сам
ps. Вы можете повторно использовать ps - например , чтобы указать на
другой выделенный new блок памяти. Вы всегда должны
обеспечивать сбалансированное применение new и delete; в
противном случае вы рискуете столкнуться с таким явлением, как
утечка памяти, то есть ситуацией, когда память выделена, но более
не может быть использована.
Если утечки памяти слишком велики, то попытка программы выделить
очередной блок может привести к ее аварийному завершению.
30

31.

Правила использования new и delete
• Не использовать delete для освобождения той памяти, которая не
было выделена new.
• Не использовать delete для освобождения одного и того же блока
памяти дважды.
• Использовать delete [ ], если для размещения массива применялея
new [ ].
• Использовать delete (без скобок), если применялея new для
размещения отдельного элемента.
• Безопасно применять delete к нулевому указателю.
31

32.

Освобождение памяти с помощью операции delete
Нельзя освобождать блок памяти, который уже был однажды
освобожден (стандарт С++ гласит, что результат таких попыток не
определен). Нельзя операцией delete освобождать память, которая
была создана объявлением обычных переменных. Обязательным
условием применения операции delete является использование его с
памятью, выделенной операцией new.
int *ps = new int;
/ / нормально
delete ps;
/ / нормально
delete ps;
/ / теперь не нормально!
int jugs = 5;
/ / нормально
int *pi = &jugs;
/ / нормально
delete pi;
/ / не допускается, память не была выделена 32
new

33.

Освобождение памяти с помощью операции delete
int *ps = new int;
/ / выделение памяти
int *pq = ps;
/ / установка второго указателя на тот же блок
delete pq;
/ / вызов delete для второго указателя
Вы не обязаны использовать тот же указатель, который был
использован с new, просто нужно использовать тот же адрес. Обычно
не стоит создавать два указателя на один и тот же блок памяти, потому
что это может привести к ошибочной попытке освобождению одного и
того же блока дважды. Однако применение второго указателя
оправдано, когда вы работаете с функциями, возвращающими
указатель.
33

34.

Использование
new
для
создания
динамических
массивов
Более типично использование new с большими фрагментами данных -
такими как массивы, строки и структуры. Именно в таких случаях
операция new весьма полезна. Предположим, например, что вы пишете
программу, которой, может быть, понадобится массив.
Распределение массива во время компиляции называется статическим
связыванием и означает, что массив встраивается в программу во
время компиляции. Но с помощью new вы можете создать массив при
необходимости, во время выполнения программы, либо не создавать
его, если потребность в нем отсутствует. Или же вы можете выбрать
размер массива уже после того, как программа запущена. Это
называется динамическим связыванием и означает, что массив будет
создан во время выполнения программы. При стат. связывании вы
должны жестко закодировать размер массива во время написания
программы. При динам. связывании программа может принять
решение о размере массива во время ее работы.
34

35.

СПРАВКА
Указатели на константные переменные
Неконстантные указатели на неконстантные значения:
int value = 7;
int *ptr = &value;
*ptr = 8; // изменяем значение value на 8
const int value = 7; // value - это константа
int *ptr = &value; // ошибка компиляции: невозможно конвертировать const int* в int*
*ptr = 8; // изменяем значение value на 8
Фрагмент кода, приведенный выше, не скомпилируется: мы не можем присвоить
неконстантному указателю константную переменную. Здесь есть смысл, ведь на то она и
константа, что её значение нельзя изменить. Гипотетически, если бы мы могли присвоить
константное значение неконстантному указателю, то тогда мы могли бы разыменовать
неконстантный указатель и изменить значение этой же константы. А это уже является
нарушением самого понятия «константа».
35

36.

СПРАВКА
Указатели на константные значения — это неконстантный указатель, который указывает на
неизменное значение. Для объявления указателя на константное значение, используется
ключевое слово const перед типом данных:
const int value = 7;
const int *ptr = &value; // здесь всё ок: ptr - это неконстантный указатель, который
указывает на "const int"
*ptr = 8; // нельзя, мы не можем изменить константное значение
В примере, приведенном выше, ptr указывает на константный целочисленный тип данных.
Пока что всё хорошо. Рассмотрим следующий пример:
int value = 7; // value - это не константа
const int *ptr = &value; // всё хорошо
Указатель на константную переменную может указывать и на неконстантную переменную (как
в случае с переменной value в примере, приведенном выше). Подумайте об этом так: указатель
на константную переменную обрабатывает переменную как константу при получении доступа
к ней независимо от того, была ли эта переменная изначально определена как const
36 или нет.

37.

СПРАВКА
Таким образом, следующее в порядке вещей:
int value = 7;
const int *ptr = &value; // ptr указывает на "const int"
value = 8; // переменная value уже не константа, если к ней получают доступ через
неконстантный идентификатор
Но не следующее:
int value = 7;
const int *ptr = &value; // ptr указывает на "const int"
*ptr = 8; // ptr обрабатывает value как константу, поэтому изменение значения
// переменной value через ptr не допускается
Указателю на константное значение, который сам при этом не является константным (он
просто указывает на константное значение), можно присвоить и другое значение:
int value1 = 7; const int *ptr = &value1; // ptr указывает на const int
int value2 = 8; ptr = &value2; // хорошо, ptr теперь указывает на другой const int
37

38.

СПРАВКА
Константные указатели — это указатель, значение которого не может быть изменено после
инициализации. Для объявления константного указателя используется ключевое слово const
между звёздочкой и именем указателя:
int value = 7; int *const ptr = &value;
Подобно обычным константным переменным, константный указатель должен быть
инициализирован значением при объявлении. Это означает, что он всегда будет указывать на
один и тот же адрес. В вышеприведенном примере ptr всегда будет указывать на адрес value
(до тех пор, пока указатель не выйдет из области видимости и не уничтожится):
int value1 = 7; int value2 = 8;
int * const ptr = &value1; // +: константный указатель инициализирован адресом value1
ptr = &value2; // -: после инициализации константный указатель не может быть изменен
Однако, поскольку переменная value, на которую указывает указатель, не является константой,
то её значение можно изменить путем разыменования константного указателя:
int value = 7; int *const ptr = &value; // ptr всегда будет указывать на value
*ptr = 8; // ок, так как ptr указывает на тип данных (неконстантный int)
38

39.

СПРАВКА
Константные указатели на константные значения
Наконец, можно объявить константный указатель на константное значение, используя
ключевое слово const как перед типом данных, так и перед именем указателя:
int value = 7;
const int *const ptr = &value;
Константный указатель на константное значение нельзя перенаправить указывать на другое
значение также, как и значение, на которое он указывает, — нельзя изменить.
Заключение. Правила
• Неконстантный указатель можно перенаправить указывать на любой другой адрес.
• С помощью указателя на неконстантное значение можно изменить это же значение (на
которое он указывает).
• Константный указатель всегда указывает на один и тот же адрес, и этот адрес не может быть
изменен.
• Указатель на константное значение обрабатывает значение как константное (даже если оно
таковым не является) и, следовательно, это значение через указатель изменить39нельзя.

40.

СПРАВКА
А вот с синтаксисом может быть немного труднее. Просто помните, что тип значения, на
который указывает указатель, всегда находится слева (в самом начале):
int value = 7;
const int *ptr1 = &value; // ptr1 указывает на "const int", поэтому это указатель на
константное значение
int *const ptr2 = &value; // ptr2 указывает на "int", поэтому это константный указатель на
// неконстантное значение
const int *const ptr3 = &value; // ptr3 указывает на "const int", поэтому это константный
// указатель на константное значение
Указатели на константные значения в основном используются в параметрах функций
(например, при передаче массива) для гарантии того, что функция случайно не изменит
значение(я) переданного ей аргумента.
40

41.

Создание динамического массива с помощью операции new
Создать динамический массив на С++ легко; вы сообщаете операции new
тип элементов массива и требуемое количество элементов.
int *psome = new int [n];/ / получить блок памяти из n элементов типа int
Операция new возвращает адрес первого элемента в блоке. В данном
примере это значение присваивается указателю psome. Когда new
применяется для создания массива, вы должны сопровождать его
альтернативной формой delete, которая указывает на то, что освобождается
массив:
delete [ ] psome ; / / освободить динамический массив
Присутствие квадратных скобок сообщает программе, что она должна
освободить весь массив, а не только один элемент, на который установлен
указатель.
41

42.

Создание динамического массива с помощью операции new
int *pt = new int;
short * ps = new short [500];
delete [ ] pt;
/ / эффект не определен, не делайте так
delete ps;
/ / эффект не определен, не делайте так
ps - это указатель на отдельный int, первый элемент блока.
Отслеживать количество элементов в блоке ложится на вашу
ответственность как разработчика. (компилятор не знает о том, что ps
указывает на первые 10 целых) На самом деле программе, конечно же,
известен объем выделенной памяти, так что она может корректно
освободить ее позднее, когда вы воспользуетесь операцией delete [ ].
Однако эта информация не является общедоступной; вы, например, не
можете использовать операцию sizeof, чтобы узнать количество
42
байт в выделенном блоке.

43.

Использование динамического массива
int * psome = new int [10] ; / / получить блок для 10 элементов типа int
Этот оператор создает указатель psome, который указывает на первый элемент блока из
10 значений int. Представьте его как палец, указывающий на первый элемент.
Предположим, int занимает 4 байта. Затем, перемещая палец на 4 байта в правильном
направлении, вы можете указать на второй элемент. Всего имеется 10 элементов, что
представляет нам допустимый диапазон, в пределах которого можно двигать палец. То
есть, операция new снабжает вас всей необходимой информацией для идентификации
каждого элемента в блоке.
Простейший способ доступа к этим элементам - используйте указатель, как если бы он
был именем массива: psome[0] вместо = *psome для первого элемента, psome[1] - для
второго и так далее.
С и С++ внутренне все равно работают с массивами через указатели. Эта
эквивалентность указателей и массивов – одно из замечательных свойств С и С++.
43

44.

Использование динамического массива
44

45.

Использование динамического массива
Фундаментальное отличие между именем массива и указателем
проявляется в следующей строке:
р3 = р3 + 1; / / допускается для указателей, но не для имен массивов
Вы не можете изменить значение для имени массива. Но указатель переменная, а потому ее значение можно изменить. Отметим эффект от
прибавления 1 к р3. Теперь выражение р3[0] ссылается на бывший второй
элемент массива. То есть, прибавление 1 к р3 заставляет р3 указывать на
второй элемент вместо первого. Вычитание 1 из значения указателя
возвращает его назад, в исходное значение, поэтому программа может
применить delete [ ] с корректным адресом. Действительные адреса
соседних элементов int отличаются на 2 или 4 байта, поэтому тот факт, что
добавление 1 к р3 дает адрес следующего элемента, говорит о том, что
арифметика указателей устроена специальным образом.
45

46.

Указатели, массивы и арифметика указателей
Прибавление единицы к целочисленной переменной увеличивает ее
значение на единицу, но прибавление единицы к переменной указателя
увеличивает ее значение на количество байт, составляющих размер
того типа, на который она указывает.
46

47.

Указатели, массивы и арифметика указателей
На заметку! Прибавление единицы к
переменной указателя увеличивает его
значение на количество байт,
представляющее размер типа, на который
он указывает.
47

48.

Указатели, массивы и арифметика указателей
48

49.

Указатели, массивы и арифметика указателей
49

50.

Указатели, массивы и арифметика указателей
При использовании нотации массивов, С++ выполняет следующее
преобразование:
array_name [i] превращается в *(array_name+i)
И если вы используете указатель вместо имени массива, С++
выполняет то же преобразование:
pointer_name [i] превращается в *(pointer_name+i)
Таким образом, во многих отношениях вы можете использовать имена
указателей и имена массивов одинаковым образом. Нотацию
квадратных скобок можно применять с обоими. К обоим можно
применять операцию разыменования ( * ) . В большинстве выражений
каждое имя представляет адрес.
50

51.

Первое отличие. Значение указателя изменить можно, а имя
массива – константа:
pointer_name = pointer_name+1; / / правильно
array_name = array_name+1; / / не допускается
Второе отличие. Применение операции sizeof к имени массива
возвращает размер массива в байтах, но применение sizeof к
указателю возвращает размер указателя, даже если он указывает
на массив. Например, в листинге как pw, так и wages ссылаются на
один и тот же массив. Однако применение операции sizeof к ним
порождает разные результаты:
2 4 = размер массива wages (отображение sizeof wages)
4 = размер указателя pw (ображение sizeof pw)
Один из случаев, когда С++ не интерпретирует имя массива как адрес.
51

52.

Адрес массива
Имя массива интерпритируется как адрес первого элемента массива, в
то время как применение операции взятия адреса приводит к выдаче
адреса целого массива:
short tell[10];
/ / создание массива из 20 байт
cout << tell << endl; / / отображение &tell[0]
cout << &tell << endl; / / отображение адреса целого массива
С точки зрения числого представления эти два адреса одинаковы, но
концептуально &tell[0] = tell – это адрес 2-байтного блока памяти,
тогда как &tell – адрес 20-байтного блока памятим. Выражение tell+1
приводит к добавлению 2 к значению адреса, а &tell+1 – к добавлению
20 к значению адреса.
52

53.

Адрес массива
tell имеет тип «указатель на short», или short*
&tell – тип «указатель на массив из 10 элементов типа short», или
short (*)[10].
short (*pas)[10] = &tell; / / pas указывает на массив из 10 элементов типа short
Если опустить круглые скобки, то правила приоритета будут
ассоциировать [10] в первую очередь с pas, делая pas массивом из 10
указателей на short, поэтому круглые скоюки необходимы. Далее, если
вы хотите описать тип переменной, вы можете воспользоваться
объявлением этой переменной в качестве руководства и удалить имя
переменной. Таким образом типом pas является short (*)[10]. Кроме
того, обратите внимание, что поскольку значение pas установлено в
&tell, а *pas эквивалентно tell, и (*pas)[0] будет первым элементом
53

54.

Динамическое хранилище
Операции new и delete предлагают более гибкий подход, нежели
использование автоматических и статических переменных. Они
управляют пулом памяти, который в С++ называется свободным
хранилищем. Этот пул отделен от области памяти, используемой
статическими и автоматическими переменными. Операции new и
delete позволяют выделять память в одной функции и освобождать
в другой. Таким образом, время жизни данных при этом не
привязывается жестко к времени жизни программы или функции.
Совместное применение new и delete предоставляет вам
возможность более тонко управлять использованием памяти, чем в
случае обычных переменных.
54

55.

Динамическое хранилище
55

56.

Динамическое хранилище
getname( ) выделяет память, а main( )
освобождает ее. Обычно это не слишком
хорошая идея - размещать new и delete в
разных функциях , потому что в этом
случае очень легко забыть вызвать
delete.
56

57.

Стеки, кучи и утечка памяти
Что случится, если вы не вызовите delete после создания переменной
операцией new в области кучи? Переменная или конструкция,
динамически выделенная в области свободного хранилища, останется
там, если не вызвать delete, даже если память, содержащая указатель,
будет освобождена в соответствии с правилами видимости и времени
жизни объектов. По сути, после этого у вас не будет никакой
возможности получить доступ к такой конструкции, находящейся в
области свободного хранилища, поскольку уже не будет существовать
указатель, который помнит ее адрес.
57

58.

Стеки, кучи и утечка памяти
В этом случае вы получите утечку памяти. Такая память остается
недоступной на протяжении всего сеанса работы программы. Она была
выделена, но не может быть освобождена. В крайних случаях (хотя и
нечастых), утечки памяти могут привести к тому, что они поглотят всю
память, выделенную программе, что вызовет ее аварийное завершение
с сообщением об ошибке переполнения памяти. Вдобавок такие утечки
могут негативно повлиять на некоторые операционные системы и
другие приложения, использующие то же пространство памяти,
приводя к их сбоям.
Даже лучшие программисты и программистские компании допускают
утечки памяти. Чтобы избежать их, лучше выработать привычку сразу
58

59.

Стеки, кучи и утечка памяти
Даже лучшие программисты и программистские компании допускают
утечки памяти. Чтобы избежать их, лучше выработать привычку сразу
объединять операции new и delete, тщательно планируя создание и
удаление конструкций, как только вы собираетесь обратиться к
динамической памяти.
На заметку! Указатели - одно из наиболее мощных средств С++.
Однако они также и наиболее опасны, потому что открывают
возможность недружественных к компьютеру действий, таких как
использование неинициализированных указателей для доступа к
памяти, либо попыток освобождения одного и того же блока
дважды.
59

60.

Операции инкремента и декремента и указатели
Вы можете использовать операции инкремента с указателями так же,
как и с базовыми переменными. Вспомним, что прибавление единицы
к указателю увеличивает его на число байт, представляющих размер
указываемого типа. То же правило остается в силе при инкременте и
декременте указателей:
double arr[5] = { 21.1, 32.8, 23.4, 45.2, 37.4) ;
double *pt = arr; / / pt указывает на arr[0], то есть на 21.1
++pt; / / pt указывает на arr[1] , то есть на 32.8
Префиксный инкремент, префиксный декремент и операция разыменования
имеют одинаковый приоритет и ассоциируются слева направо.
Постфиксный инкремент и декремент имеют одинаковый приоритет, более
высокий, чем приоритет префиксных форм. Эти две операции также
60

61.

Операции инкремента и декремента и указатели
* ++pt; / / увеличить указатель , получить значение; то есть arr[2], или
23.4
В * ++pt сначала применяется ++ к pt (потому что ++ находится справа
от *) , а затем применяется * к новому значению pt.
++ *pt; / / увеличить указываемое значение ; то есть изменить 23.4 на
24.4
++ *pt означает - получить значение, на которое указывает pt, а затем
увеличить указатель. Здесь pt по-прежнему будет указывать на arr[2].
(* pt) ++; / / увеличить указатель на значение
Скобки указывают, что сначала выполняется разыменование указателя,
порождая значение 24.4. Затем операция ++ увеличивает значение до
61

62.

Операции инкремента и декремента и указатели
*pt++; // разыменовать исходный указатель, затем увеличить его.
Более высокий приоритет постфиксной операции ++ означает, что ++
увеличит pt, а не *pt, поэтому инкремент касается указателя. Но тот
факт, что использована постфиксная операция, означает, что
разыменаванный адрес является адресом &arr[2], а не новым адресом.
То есть значение *pt++ равно arr[2], или 25.4, но после завершения
оператора pt будет указывать на arr[3].
На память! Инкремент и декремент указателей следуют правилам
арифметики указателей. Если pt указывает на первый элемент массива,
++pt изменяет его так, что он уже указывает на второй его элемент.
62

63.

Функции и массивы
Опишем прототип функции, возвращающей сумму элементов массива
int sum_arr( int arr[ ] , int n) / / arr = имя массив а, n = размер или
int sum_arr ( int * arr, int n) // arr = имя массива , n = размер
Квадратные скобки указывают на то, что arr - массив, а тот факт, что
они пусты, говорит о том, что вы можете применять эту функцию с
массивами любого размера. arr - на самом деле не массив, а указатель!
63

64.

Функции и массивы
64

65.

Функции и массивы
Последствия использования массивов в качестве аргументов
Вызов функции sum_arr (cookies, ArSize) передает адрес первого элемента массива cookies и количество
его элементов в функцию sum_arr( ). Функция sum_arr( ) присваивает адрес cookies переменной указателю arr, а значение ArSize - переменной n типа int . В функцию не передается содержимое
массива. Вместо этого программа сообщает функции, где находится массив (то есть сообщает адрес),
65
каков тип его элементов (тип массива) и сколько в нем содержится элементов ( переменная n).

66.

Отображение массива и защита его посредством const
Что бы предотвратить случайное изменение содержимого массивааргумента, вы можете использовать ключевое слово const, объявляя
формальный аргумент:
void show_array(const double ar[ ], int n) ;
Это объявление устанавливает, что указатель ar указывает на
константные данные. Это значит, что вы не можете применить ar для
изменения данных. То есть вы можете обратиться к такому значению
как к ar[0], но не можете его изменять. Следует отметить, что это вовсе
не означает, что исходный массив должен быть константным; это
значит лишь, что вы не можете использовать ar внутри функции
show_array( ) для изменения данных. Другими словами, show array( )
66

67.

Отображение массива и защита его посредством const
Предположим, вы нечаянно нарушили это ограничение, попытавшись
внутри функции show_array ( ) сделать что-то вроде такого:
ar[0] += 10;
В этом случае компилятор пресечет ваши " преступные" действия.
Компилятор С++ выдаст сообщение об ошибке.
Задача. Предположим, что вы хотите использовать массив для
отслеживании стоимости вашей недвижимости (предположим, что
она у вас есть).
67

68.

69.

70.

71.

Мы начали с обдумывания типа данных и
разработали соответствующий набор функций для их
обработки. Затем мы встроили эти функции в
программу. Это то, что иногда называют
программированием снизу вверх (восходящим
программированием), поскольку процесс дизайна
идет от частей-компонентов к целому. Этот подход
хорошо стыкуется с ООП, которое сосредоточено в
первую очередь на данных и манипуляциях ими.
Традиционное процедурное программирование, с
другой
стороны,
следует
парадигме
программирования сверху вниз (нисходящей),
когда вы сначала разрабатываете укрупненный
модульный дизайн, а затем обращаете свое внимание
на детали. Оба метода полезны, и оба ведут к
модульным программам.

72.

Функции, работающие с диапазонами массивов
Традиционный подход С/С++ к функциям, обрабатывающим массивы,
состоит в передаче указателя на начало массива в одном аргументе и
размера массива - в другом.
Другой подход - указать диапазон элементов. Это можно сделать,
передав два указателя - один, идентифицирующий начальный элемент
массива, и второй , указывающий его конец.
Стандартная библиотека шаблонов С++ (STL), например, обобщает
такой подход с применением диапазона. Подход STL использует
концепцию "следующий после конца" для указания границы
диапазона. То есть в случае массива аргумент, идентифицирующий
конец массива, должен быть указателем, установленным на адрес,
72

73.

Функции, работающие с диапазонами массивов
Например, предположим, что имеется такое объявление:
double elbuond[20];
Тогда два указателя, elbuond и elbuond+20, определяют диапазон.
Первый, elbuod - это имя массива; он указывает на первый элемент.
Выражение elbuod+19 указывает на последний элемент (то есть elbuod
[l9]), поэтому elbuod+20 указывает на элемент, следующий сразу за
последним. Передавая функции диапазон, вы сообщаете ей, какие
элементы следует обрабатывать.
73

74.

75.

Разные вызовы функций задают различные диапазоны в пределах массива:
int sum = sum_arr (cookies, cookies+ArSize);
...
sum = sum_arr (cookies, cookies+3);
/ / первые 3 элемента
...
sum = sum_arr (cookies+4, cookies+8);
/ / 4 последних элемента
Кстати, обратите внимание, что правило вычитания указателей предполагает, что в sum_arr( ) выражение
end - begin возвращает количество элементов в диапазоне.

76.

Функции изменения размера массива
void push_back(int *&arr, int &size, const int value)
{
int value – значение, которое должно быть
записанно в конец массива
int *mas=new int [size+1];
int &size – размер массива. Т.к. внутри функции
for (int i=0;i<size;i++)
размер массива изменится, то мы должны
увидеть эти изменения в main, поэтому
mas[i]=arr[i];
передаем переменную по ссылке.
mas[size]=value;
int *&arr – указатель на ссылку, дает
size++;
возможность подминить нашему указателю,
передаваемому в функцию, который указывает
delete [] arr;
на область оперативной памяти, подменить ему
arr=mas;
адрес, чтобы он указывал на новый массив.
}
Когда мы передаем просто указатель на массив,
то мы работаем с копией этого указателя и при
выходе из функции в main информации об
изменении адреса не была бы видна.
76

77.

Функции изменения размера массива
void pop_back(int *&arr, int &size)
{
size--;
int *mas=new int [size];
for (int i=0;i<size;i++)
mas[i]=arr[i];
delete [] arr;
arr=mas;
}
int value – значение, которое должно быть
записанно в конец массива
int &size – размер массива. Т.к. внутри функции
размер массива изменится, то мы должны
увидеть эти изменения в main, поэтому
передаем переменную по ссылке.
int *&arr – указатель на ссылку, дает
возможность подминить нашему указателю,
передаваемому в функцию, который указывает
на область оперативной памяти, подменить ему
адрес, чтобы он указывал на новый массив.
Когда мы передаем просто указатель на массив,
то мы работаем с копией этого указателя и при
выходе из функции в main информации об
изменении адреса не была бы видна.
77

78.

Рекурсивный вызов функции
Если функция в процессе работы вызывает саму себя, то имеет место рекурсия, а функция
называется рекурсивной. Рекурсия – важный инструмент для некоторых областей программирования,
таких как искусственный интеллект. Основные принципы работы.
• Рекурсия с одиночным рекурсивным вызовом
Если рекурсивная функция вызывает саму себя, затем этот новый вызов снова вызывает себя и так далее,
то получится бесконечная последовательность вызовов, если только код не включает в себе нечто, что
позволит прервать эту цепочку вызовов. Обычный метод состоит в том, что рекурсивный вызов заключают
в оператор if. Например:
void recurs( списокАргументов )
{
операторыl
if ( проверка )
recurs (аргументы)
операторы2
}
В какой то ситуации проверка возвращает false, и цепочка вызовов прекращается. Рекурсивные вызовы порождают замечательную цепочку
событий. До тех пор, пока условие оператора if остается истинным, каждый вызов recurs( ) выполняет операторыl и затем вызывает новое
воплощение recurs( ), не достигая конструкции операторы2. Когда условие оператора if возвращает false, текущий вызов переходит к
операторы2. Затем, когда текущий вызов завершается, управление возвращается предыдущему экземпляру recurs( ), который вызвал его. Затем
этот экземпляр исполняет свой раздел операторы2 и прекращается, возвращая управление предшествующему вызову, и так далее. Таким
образом, если происходит пять вложенных вызовов recurs( ), то первый раздел операторы1 выполняется пять раз в том порядке, в котором
произошли вызовы, а потом пять раз в обратном порядке выполняется раздел операторы2. После входа в пять уровней рекурсии
78 затем
программа должна пройти обратно эти же пять уровней.

79.

Обратите внимание , что каждый рекурсивный
вызов создает свой собственный набор
переменных, поэтому на момент пятого вызова
она имеет пять отдельных переменных по имени
n - каждая со своим собственным значением.
cout << "Обратный отсчет . . . "<< n << "(n по адресу" << &n << " ) " << endl ;
cout << n < < " : Бабах ! " ; < < " ( n по адресу " << & n < < " ) " < < endl ;
Обратите внимание, что n, имеющая значение 4,
размещается в одном месте (в данном примере по
адресу 0012FEOC), n, имеющая значение 3,
находится в другом месте (адрес памяти
0012FD34) и так далее.
79

80.

Функция с переменным числом аргументов
• #include <stdarg.h>
• int summa (int n, …)
{
int s=0;
//……
return s;
}
• sum = summa(3, 12, 13, 14);
80

81. Функция как производный тип. Указатель на функцию (начало)

• Функция как производный тип языка введена в Си для решения задач, в которых функция
(ее адрес) должна являться операндом некоторых операций, параметром другой функции
или возвращаемым другой функцией результатом
• Указатель на функцию — адресное выражение (в частном случае — переменная-указатель),
значением которого выступает адрес первого байта (первого машинного слова)
исполнимого кода функции
• Значением идентификатора любой функции, введенного в ее определении (прототипе),
является константный указатель на эту функцию
• Определение переменной-указателя на функцию с конкретной спецификацией
параметров
<тип результата> (*<идентификатор>)
([<спецификация формальных параметров>]);
81

82. Функция как производный тип. Указатель на функцию (продолжение)

• Три формы записи операции вызова функции ()
• явный вызов по имени (константному указателю)
<имя функции> (<список фактических параметров>)
• вызов с разыменованием неконстантного указателя
(*<идентификатор>)(<список фактических параметров>)
• вызов без разыменования неконстантного указателя
<идентификатор>(<список фактических параметров>)
• Определение одномерного массива указателей на функции с одинаковой спецификацией
параметров
<тип результата> (*<идентификатор>[<размер>])
([<спецификация формальных параметров>]);*, **
* — здесь парные квадратные скобки, показанные моноширинным шрифтом: [<размер>], являются элементом грамматики; парные
квадратные скобки, показанные пропорциональным шрифтом: [<спецификация формальных параметров>], являются метасимволом и по
традиции обозначают необязательный грамматический элемент;
** — отсутствие ограничений на построение многомерных массивов указателей на функции в языке Си позволяет индуктивно расширить
данное определение так же, как для любого другого типа элементов массива (см. модуль №6)
82

83. Функция как производный тип. Указатель на функцию (окончание)

• Допустимые операции над указателями на функции
• присваивание значения (=) (только для константных и переменных указателей на функции того же типа — с
совпадающими типами всех формальных параметров и возвращаемого значения);
• разыменование (косвенная адресация) (*);
• получение адреса указателя (&);
• операции сравнения с указателями на функции того же типа (см. выше) или константой NULL (==, !=)
• Арифметические операции над указателями на функции запрещены
83

84. Указатели и массивы

• Согласно синтаксису языка Си идентификатор массива без индексов элементов — это
указатель-константа со значением адреса нулевого (имеющего индекс [0]) элемента
массива
• С учетом адресной арифметики, обращение к произвольному элементу массива данных
любого типа может производиться одним из двух равнозначных способов, которые
допускается комбинировать в случае многомерных массивов:
<идентификатор массива>[<индекс элемента>]
*(<идентификатор массива> + <индекс элемента>)
84
English     Русский Rules