PDA

Просмотр полной версии : [Урок] [Цикл уроков программиста] 14. Указатели



Tracker1
30.07.2013, 20:36
Начнём, как обычно, издалека.
Когда операционная система запускает программу которую вы написали, все данные относящиеся к программе помещаются в оперативную память. Как вы, наверное, помните (из урока про типы данных), память в компьютере состоит из байтов. Байты в памяти нумерются, т.е. у каждого байта есть порядковый номер - его адрес. С помощью адреса процессор может обратиться к любому байту в памяти. Адреса хранятся в шестнадцатиричном формате. Обычно мы используем десятичную систему. В компьютерах удобнее всего использовать двоичную (числа формируются всего из двух цифр: 0 и 1), или степень двойки (16 = 24). При этом в шестнадцатиричной системе 16 цифр: 0, 1, ... 9, A, B, C, D, E, F (где цифры от a до f соответствуют десятичным 10-15). Адрес выглядит примерно так: 0x0012fc2c, что равно числу 1244204 в десятичной системе счисления. 0x (ноль икс) перед адресом говорит, что число шестнадцатиричное.

Ну так вот, у каждого байта есть свой адрес. Когда ваша программа попадает в оперативную память, всем данным присваиваются адреса, чтобы процессор мог к ним обращаться. Каким данным присваиваются адреса: переменным, функциям, структурным переменным, объектам классов.

При этом данные хранятся в двух разных участках памяти: стеке и куче:


int a = 0;

int main()
{
int b = 0;
return 0;
}


a хранится в куче (heap), b хранится в стеке (stack). Все глобальные переменные попадают в кучу, все локальные переменные попадают в стек. что такое стек, мы рассмотрели в прошлом уроке. Куча же - это область памяти где данные никак не организованы.

Теперь, рассмотрим простое объявление переменной:


int a;


a - идентификатор с помощью которого мы можем обращаться к какой-то области памяти. При этом данная переменная занимает четыре байта. Обращение к переменной происходит по первому байту.

При создании переменной a, ей присваивается адрес 0x0012fc28 (адрес взят для примера). Процессор обращается к этой переменной с помощью этого адреса. Сама же переменная занимает в памяти следующие байты: 0x0012fc28, 0x0012fc29, 0x0012fc2a, 0x0012fc2b (обратите внимание на последние цифры в адресах). Где первый байт - адрес переменной.

Теперь мы плавно переходим к рассмотрению указателей.

Следует заметить, что указатели есть не во всех языках программирования.

Указатель (pointer) - это переменная, значением которой является адрес.

Чтобы получить адрес обычной переменной можно воспользоваться следующим синтаксисом:


cout << &a;


На экран будет выведен адрес переменной a.

Указатель можно создать следующим образом:


int a = 0;
int* ptr;

ptr = &a;

cout << ptr;


Здесь & - операция получения адреса. Не путайте использование & для получения адреса и для передачи в функцию значения по ссылке.

В данном примере мы создали указатель ptr на тип int и присвоили этому указателю адрес, где хранится переменная a.

Когда вы создаёте указатель, всегда инициализируйте его каким-нибудь значением. По умолчанию в указателе может храниться любой адрес, и если вы попытаетесь изменить значение в этом адресе, то вполне вероятно возникновение ошибки при выполнении программы.

Через указатель можно изменять значение хранящееся по адресу. Для этого указатель нужно разыменовать:


int a = 0;
int* ptr = &a;

*ptr = 10; // то же самое что и: a = 10;

cout << a; вывод на экран: 10


Здесь мы воспользовались указателем, чтобы изменить значение переменной a. В данном случае * - операция разыменования. Не путайте с объявлением указателя!

При разыменовании указателя мы получаем доступ к значению адреса в памяти, на который указывает указатель.
Указатель на тип void

До сих пор мы использовли ключевое слово void (void - недействительный, пустой) в заголовках функций, когда нам не нужно было возвращать никаких значений.

Указатели на разные типы не могут использоваться друг с другом:


int a = 0;
float b = 1;

int* ptr_a = &a;
float* ptr_b = &b;
// ptr_a = ptr_b; // Так нельзя!!!


Чтобы обойти это ограничение можно воспользоваться указателем на void:


int a = 0;
float b = 1;
char c = 2;

int* ptr_a = &a; // Указатель на тип int
float* ptr_b = &b; // Указатель на тип float
void* ptr_c = &c; // указатель на void

// ptr_a = ptr_b; // Так нельзя!!!

ptr_c = ptr_a; // Оба варианта
ptr_c = ptr_b; // корректны


Указатели на void особенно полезны при использовании с классами. В DirectX очень многие объекты создаются как указатели на void.
Указатели-константы

Мы уже не раз встречались с указателями-константами. Это такие указатели, значение которых не может быть изменено. Т.е. не могут быть изменены адреса. Значения же хранящиеся в адресах могут изменяться.

Более известное имя указателей-констант - массивы.

Хотя надо заметить, что данный вид указателей используется не только с массивами:


int a = 1;
int b = 5;
int* ptr_a = &a;
int* const ptr_b = &b;
*ptr_b = 2;
// ptr_b = ptr_a; // так не получится. ptr_b - константа
*ptr_b = 3;

Передача аргумента в функцию по указателю

Мы уже умеем передавать аргументы в функции двумя способами: по значению и по ссылке. Третий способ - передача по указателю (pass-by-pointer).


int main()
{
int a = 10;
pass_by_pointer(&a); // Передача адреса переменной
cout << a; // 5
return 0;
}

void pass_by_pointer (void* ptr)
{
*ptr = 5;
}


При передаче по указателю мы передаём адрес. Внутри функции параметр ptr может непосредственно влиять на содержимое внешней переменной a.
Операция new

В программе pseudo_game мы использовали заданный массив. Мы не могли сделать что-нибудь подобное:


cin >> s; // количество строк
cin >> c; // количество столбцов

char map[s][c];


Компилятор должен знать заранее (до начала выполнения программы) сколько памяти выделить на массив. То же самое и с классами: все объекты классов должны быть созданы до начала выполнения программы. Вы не можете динамически создать объект.

Операция new позволяет обойти это ограничение. (В примере используется класс tank из программы pseudo_game):


/*
Здесь представлены сразу два примера:
создание указателя на tank
и
выделение памяти для хранения всех клеток игрового поля
*/

tank* t34 = new tank; // выделение памяти под объект tank
// Здесь t34 - указатель на тип tank

int s,c; // количество строк и столбцов
cin >> s;
cin >> c;


char** map; // указатель
map = new char*[s];// указателю присваем адресс массива указателей char*
for(int i=0;i<s;i++) map[i]=new char[c];


Здесь t34 и map - указатели.

В данном примере мы вынуждены использовать одномерный массив для представления двумерного. Как в данном случае получить доступ к произовльному элементу? Вот как выглядит инициализация клетки (5,6):


map[5][6]='T';


Думаю, данный пример нуждается в пояснениях. Мы создаем указатель на массив указателей типа char.
Потом мы должны каждому указателю присвоить массив char. Это очень тяжело на первый взгляд. Попробуй-те запустить этот пример и затем отследить его отладчиком
Операция delete

Операция delete используется для освобождения памяти после того как она стала не нужна, дабы не допустить утечки памяти:


for(int i=0;i<s;i++) delete [] map[i];
delete [] map;

delete t34;


Доступ к объектам

Последнее на что хочу обратить внимание. Доступ к членам класса объекта созданного с помощью указателей отличается от того, что мы видели раньше.

t34->fuel = 100;

То есть вместо точки используются символы ->.

Прошу заметить, что динамический двумерный массив я написал сам, а не автор урока.
Причина - сильно устарелый метод. Для того чтобы вам лучше понять указатели, я тоже написал на указателях.
Есть более современный и простой метод для написания динамических массивов, но он требует библиотеку std и будет работать лишь в С++.


#include <vector>
using namespace std;
int main()
{
vector<int> x; //vector - объект. <int> - тип данных хранимых в этом объекте. x - название объекта
x.push_back(5); // занести в массив x пятерку. Теперь x[0] = 5.
vector<int> array(4,100); // массив с четырьмя элементами равных 100.
array.resize(10); //расширить массив до 10 ячеек. Теперь массив выглядит так 100,100,100,100,0
}


На сегодня всё.