
Указатель на функцию
Указатель на функцию содержит адрес в сегменте кода, по которому располагается исполняемый код функции, то есть адрес, по которому передается управление при вызове функции. Указатели на функции используются для косвенного вызова функции (не через ее имя, а через обращение к переменной, хранящей ее адрес), а также для передачи имени функции в другую функцию в качестве параметра. Указатель функции имеет тип «указатель функции, возвращающей значение заданного типа и имеющей аргументы заданного типа»:
тип (*имя) ( список_типов_аргументов );
Например, объявление:
int (*fun) (double, double);
задает указатель с именем fun на функцию, возвращающую значение типа int и имеющую два аргумента
типа double.
Указатель на объект
Указатель на объект содержит адрес области памяти, в которой хранятся данные определенного типа (основного или составного). Простейшее объявление указателя на объект (в дальнейшем называемого просто указателем) имеет вид:
тип *имя;
где тип может быть любым, кроме ссылки и битового поля, причем тип может быть к этому моменту только объявлен, но еще не определен (следовательно, в структуре, например, может присутствовать указатель на структуру того же типа).
Звездочка относится непосредственно к имени, поэтому для того, чтобы объявить несколько указателей, требуется ставить ее перед именем каждого из них. Например, в операторе
int *a, b, *c;
описываются два указателя на целое с именами а и с, а также целая переменная b.
Размер указателя зависит от модели памяти. Можно определить указатель на указатель и т. д.
Указатель на void
Указатель на void применяется в тех случаях, когда конкретный тип объекта, адрес которого требуется хранить, не определен (например, если в одной и той же переменной в разные моменты времени требуется хранить адреса объектов различных типов).
Указателю на void можно присвоить значение указателя любого типа, а также сравнивать его с любыми указателями, но перед выполнением каких-либо действий с областью памяти, на которую он ссылается, требуется преобразовать его к конкретному типу явным образом.
Примеры объявления указателей
Указатель может быть константой или переменной, а также указывать на константу или переменную. Рассмотрим примеры:
int i; // целая переменная
const int ci = 1; // целая константа
int * pi; // указатель на целую переменную
const int * pci; // указатель на целую константу
int * const ср = &i; // указатель-константа на целую переменную
const int * const срс = &ci; // указатель-константа на целую константу
Ссылки в C++
Ссылка представляет собой синоним имени, указанного при инициализации ссылки. Ссылку можно рассматривать как указатель, который всегда разыменовывается. Формат объявления ссылки:
тип & имя;
где тип — это тип величины, на которую указывает ссылка, & — оператор ссылки, означающий, что следующее за ним имя является именем переменной ссылочного типа, например:
int коl;
int& pal = kol; // ссылка pal - альтернативное имя для коl
const char& CR = '\n '; // ссылка на константу
Запомните следующие правила:
1)Переменная-ссылка должна явно инициализироваться при ее описании, кроме случаев, когда она является параметром функции, описана как extern или ссылается на поле данных класса.
2)После инициализации ссылке не может быть присвоена другая переменная.
3)Тип ссылки должен совпадать с типом величины, на которую она ссылается.
4)Не разрешается определять указатели на ссылки, создавать массивы ссылок и ссылки на ссылки.
Ссылки применяются чаще всего в качестве параметров функций и типов возвращаемых функциями
значений. Ссылки позволяют использовать в функциях переменные, передаваемые по адресу, без операции разадресации, что улучшает читаемость программы.
Ссылка, в отличие от указателя, не занимает дополнительного пространства в памяти и является просто другим именем величины. Операция над ссылкой приводит к изменению величины, на которую она ссылается.
Динамическая память
Выделяется и освобождается — с помощью специальных инструкций (т. е. по инициативе разработчика). Это позволяет по ходу работы программы контролировать и корректировать объём используемой памяти и, следовательно, создавать программы способные обрабатывать большие объёмы данных, обходя ограниченность физической памяти машины.
Доступ к динамической памяти возможен только через указатели, т. е. нельзя создавать в неё переменные, но можно выделять её фрагменты и связывать из с некоторым указателем.
Работа с динамической памятью
Выделяется память с помощью оператора new, а освобождается — с помощью оператора delete.
В момент, когда динамическая память выделена, она должна быть связана с некоторым указателем, подходящего типа (при выделении указывается тип и количество необходимых ячеек данного типа).
int* p;
p = new int;
*p = 10;
cout << *p; // 10
delete p; // память освобождена
Если не освобождать динамическую память, то она будет занята до завершения программы, что неприемлемо.
При выделении одной динамической переменной (одной ячейки памяти), можно сразу инициализировать её значение:
int* p;
p = new int(10);
cout << *p; // 10
delete p; // память освобождена
Можно выделять сразу несколько ячеек динамической памяти, получая динамический массив.
Для этого его размер указывается в квадратных скобках после типа.
Чтобы удалить динамический массив и освободить память используется оператор delete[].
int* p;
p = new int[13];
for (int i=0; i<13; i++) {
*(p+i) = i + 1;
cout << *(p+i) << ' '; // 1 2 3 ... 13
}
delete[] p; // память освобождена, из неё удалены все элементы
Cразу после создания динамический массив автоматически заполняется нулями (в отличии от обычного массива в статической или стековой памяти).
Если в указатель, уже хранящий адрес какого-то фрагмента динамической памяти, записать новый адрес, то фрагмент динамической памяти будет потерян, т. е. он не будет освобождён, но к нему никак нельзя будет обратиться (например, чтобы освободить этот фрагмент).
int* p;
p = new int(13);
int a = 666;
p = &a; // теперь до 13 никак не добраться
Проблема становится особенно острой, когда в памяти теряются целые массивы (они занимают больше места, чем отдельные переменные).
int* p;
for (int i=1; i<=10; i++) {
p = new int[100];
}
delete[] p;
На каждом шаге цикла создаётся динамический массив из 100 элементов. Всего таких массивов будет создано 10, но только от последнего из них память будет освобождена после выхода из цикла. 9 массивов продолжат занимать место в памяти до конца программы. 9 массивов * 100 элементов * 4 байта = 3600 байт потерянной памяти, которую никак нельзя использовать (ни в этой программе, не в других запущенных).
Очень важно после использования динамической памяти не забывать освобождать её в нужный момент!
8.Совместимость и преобразование типов. Явное и неявное преобразование.
Два типа считаются совместимыми, если:
оба они есть один и тот же тип;
оба вещественные;
оба целые;
один тип есть тип-диапазон второго типа;
оба являются типами-диапазонами одного и того же базового типа;
оба являются множествами, составленными из элементов одного и того же базового типа;
оба являются упакованными строками (определены с предшествующим словом PACKED) одинаковой максимальной длины;
один тип есть тип-строка, а другой - тип-строка, упакованная строка или символ;
один тип есть любой указатель, а другой - нетипизированный указатель;
один тип есть указатель на объект, а другой - указатель на родственный ему объект;
оба есть процедурные типы с одинаковыми типом результата (для типа-функции), количеством параметров и типом взаимно соответствующих параметров.
Совместимость типов приобретает особое значение в операторах присваивания. Пусть T1 - тип переменной, а Т2 - тип выражения, т.е. выполняется присваивание T1 := T2. Это присваивание возможно в следующих случаях: T1 и T2 есть один и тот же тип и этот тип не относится к файлам или массивам файлов, или записям, содержащим поля-файлы, или массивам таких записей;
T1 и T2 являются совместимыми порядковыми типами и значение T2 лежит в диапазоне возможных значений T1;
T1 и T2 являются вещественными типами и значение T2 лежит в диапазоне возможных значений T1;
T1 - вещественный тип и T2 - целый тип; ,
T1 - строка и T2 - символ;
T1 - строка и T2 - упакованная строка;
T1 и T2 - совместимые упакованные строки;
T1 и T2 - совместимые множества и все члены T2 принадлежат множеству возможных значений T1;
T1 и T2 - совместимые указатели;
T1 и T2 - совместимые процедурные типы;
T1 - объект и T2 - его потомок.
В программе данные одного типа могут преобразовываться в данные другого типа. Такое преобразование может быть явным или неявным.
Явное и неявное преобразование
В C++ различают явное и неявное преобразование типов данных. Неявное преобразование типов данных выполняет компилятор С++, ну а явное преобразование данных выполняет сам программист. О преобразовании типов данных скажу следующее: «Результат любого вычисления будет преобразовываться к наиболее точному типу данных, из тех типов данных, которые участвуют в вычислении». Для наглядного примера представлю таблицу с преобразованиями типов данных. В таблице рассмотрим операцию деления. В качестве целочисленного типа данных возьмем int, ну и вещественный тип данных у нас будет float.
Таблица 1 — Явное и неявное преобразование типов данных в С++
x y Результат деления Пример
делимое делитель частное x = 15 y = 2
int int int 15/2=7
int float float 15/2=7.5
float int float 15/2=7.5
Из таблицы видно, что меняя переменные различных типов данных местами, результат остается тот же (в нашем случае это делимое и делитель). О неявном преобразовании типов данных все. Что же касается явного преобразования, то оно необходимо для того чтобы выполнять некоторые манипуляции, тем самым меняя результат вычисления. Самый простой способ явного преобразования типов данных, пример: допустим нам необходимо разделить такие числа 15 и 2, делим! 15/2=7. Результат тот же, что и в таблице. Но если сделать незначительные преобразования, например: 15.0/2=7.5 при таком делении число 15 является вещественным, значит и результат будет вещественный. Само число 15 с точки зрения математики не изменилось, ведь 15=15.0. Этот же прием можно было применить к двойке, результат был бы тем же, а можно было сразу к двум числам, но зачем ,если хватает одного из двух.
Еще один способ явного преобразования типов данных:
float(15) / 2 // результат равен 7.5, число 15 преобразуется в вещественный тип данных float.
double(15) / 2 // результат равен 7.5 – тоже самое!!!