При выполнении любой программы, все необходимые для ее работы данные должныбыть загружены в оперативную память компьютера. Для обращения к переменным, находящимся в памяти, используются специальные адреса, которые записываются в шестнадцатеричном виде, например 0x100 или 0x200.
Если переменных в памяти потребуется слишком большое количество, которое не сможет вместить в себя сама аппаратная часть, произойдет перегрузка системы или её зависание.
Если мы объявляем переменные статично, так как мы делали в предыдущих уроках, они остаются в памяти до того момента, как программа завершит свою работу, а после чего уничтожаются.
Такой подход может быть приемлем в простых примерах и несложных программах, которые не требуют большого количества ресурсов. Если же наш проект является огромным программным комплексом с высоким функционалом, объявлять таким образом переменные, естественно, было бы довольно не умно.
Можете себе представить, если бы небезызвестная Battlefield 3 использовала такой метод работы с данными? В таком случае, самым заядлым геймерам пришлось бы перезагружать свои высоконагруженные системы кнопкой reset после нескольких секунд работы игры.
Дело в том, что играя в тот же Battlefield, геймер в каждый новый момент времени видит различные объекты на экране монитора, например сейчас я стреляю во врага, а через долю секунды он уже падает убитым, создавая вокруг себя множество спецэффектов, таких как пыль, тени, и т.п.
Естественно, все это занимает какое-то место в оперативной памяти компьютера. Если не уничтожать неиспользуемые объекты, очень скоро они заполнят весь объем ресурсов ПК.
По этим причинам, в большинстве языков, в том числе и C/C++, имеется понятие указателя. Указатель — это переменная, хранящая в себе адрес ячейки оперативной памяти, например 0x100.
Мы можем обращаться, например к массиву данных через указатель, который будет содержать адрес начала диапазона ячеек памяти, хранящих этот массив.
После того, как этот массив станет не нужен для выполнения остальной части программы, мы просто освободим память по адресу этого указателя, и она вновь станет доступно для других переменных.
Ниже приведен конкретный пример обращения к переменным через указатель и напрямую.
Пример использования статических переменных
# include <iostream>
using namespace std;
int main()
{
int a; // Объявление статической переменной
int b = 5; // Инициализация статической переменной b
a = 10;
b = a + b;
cout << "b is " << b << endl;
return 0;
}
Пример использования динамических переменных
# include <iostream>
using namespace std;
int main()
{
int *a = new int; // Объявление указателя для переменной типа int
int *b = new int(5); // Инициализация указателя
*a = 10;
*b = *a + *b;
cout << "b is " << *b << endl;
delete b;
delete a;
return 0;
}
Синтаксис первого примера вам уже должен быть знаком. Мы объявляем/инициализируем статичные переменные a и b, после чего выполняем различные операции напрямую с ними.
Во втором примере мы оперируем динамическими переменными посредством указателей. Рассмотрим общий синтаксис указателей в C++.
Выделение памяти осуществляется с помощью оператора new и имеет вид: тип_данных *имя_указателя = new тип_данных;, например int *a = new int;. После удачного выполнения такой операции, в оперативной памяти компьютера происходит выделение диапазона ячеек, необходимого для хранения переменной типа int.
Логично предположить, что для разных типов данных выделяется разное количество памяти. Следует быть особенно осторожным при работе с памятью, потому что именно ошибки программы, вызванные утечкой памяти, являются одними из самых трудно находимых. На отладку программы в поисках одной ничтожной ошибки, может уйти час, день, неделя, в зависимости от упорности разработчика и объема кода.
Инициализация значения, находящегося по адресу указателя выполняется схожим образом, только в конце ставятся круглые скобки с нужным значением: тип данных *имя_указателя = new тип_данных(значение). В нашем примере это int *b = new int(5).
Для того, чтобы получить адрес в памяти, на который ссылается указатель, используется имя переменной-указателя с префиксом &. перед ним (не путать со знаком ссылки в C++).
Например, чтобы вывести на экран адрес ячейки памяти, на который ссылается указатель b во втором примере, мы пишем cout << "Address of b is " << &b << endl;. В моей системе, я получил значение 0x1aba030. У вас оно может быть другим, потому что адреса в оперативной памяти распределяются таким образом, чтобы максимально уменьшить фрагментацию. Поскольку, в любой системе список запущенных процессов, а также объем и разрядность памяти могут отличаться, система сама распределяет данные для обеспечения минимальной фрагментации.
Для того, чтобы получить значение, которое находится по адресу, на который ссылается указатель, используется префикс *. Данная операция называется разыменованием указателя.
Во втором примере мы выводим на экран значение, которое находится в ячейке памяти (у меня это 0x1aba030): cout << "b is " << *b << endl; . В этом случае необходимо использовать знак *.
Чтобы изменить значение, находящееся по адресу, на который ссылается указатель, нужно также использовать звездочку, например, как во втором примере — *b = *a + *b;.
- Когда мы оперируем данными, то используем знак *
- Когда мы оперируем адресами, то используем знак &
В этих вещах очень часто возникают недопонимания, и кстати, не только у новичков. Многие из тех, кто начинал программировать с того же php, также часто испытывают подобную путаницу при работе с памятью.
Для того, чтобы освободить память, выделенную оператором new, используется оператор delete.
Пример освобождения памяти
# include <iostream>
using namespace std;
int main()
{
// Выделение памяти
int *a = new int;
int *b = new int;
float *c = new float;
// ... Любые действия программы
// Освобождение выделенной памяти
delete c;
delete b;
delete a;
return 0;
}
При использовании оператора delete для указателя, знак * не используется.