Урок 8: «Первое знакомство с указателями»


Вы уже должны уметь писать простейшие программы на C++. Теперь вы знаете, что такое массивы, функции, переменные и как все это использовать. С этого урока можно сказать, что начинается вторая часть изучения, немного сложнее предыдущей.

В этом уроке мы коснемся, правда пока еще поверхностно, с такими средствами C, как указатели и ссылки. Это очень важная тема, поэтому внимательно читайте урок.

Указатель – это переменная, содержащая адрес памяти, в которой содержится другая переменная. Как это понять? Вы уже знаете, что для каждой переменной вашей программы компилятор выделяет место в памяти. Так вот, указатель как раз содержит адрес этого места. Давайте рассмотрим это на примере.

У нас есть число 27 типа int. Мы используем переменную a для хранения этого числа. В стеке программы выделилось место для хранения этой переменной (что такое стек мы рассмотрим в уроках с динамическими структурами данных). Графически это можно представить так:

Стек

27

 

Я сильно абстрагируюсь, но примерно в памяти это будет так. Как видите, у меня на рисунке наша переменная содержится во второй ячейке. Естественно, что компьютер переведет для себя в 16 ричный код адрес этой ячейки. И теперь, если воспользоваться указателем и присвоить ему значение этого адреса, то мы получим доступ к этой ячейке. Немного непонятно, не так ли?

Дело в том, что C++, как расширение своего предшественника C, является, как выразился Денис Ричи (создатель C), языком «среднего уровня». В компьютерной литературе вы тщетно можете искать определение этого термина –его просто нет. Тем не менее, C был компромиссом между низкоуровневым языком и высокоуровневым. Дело в том, что раньше системные программы писались на ассемблере, где напрямую можно обращаться к регистрам процессора (регистр в ассемблере это что-то типа переменной в широком смысле этого слова). Если вы никогда не писали программы на ассемблере, поздравляю – вы не убили часть своей жизни на бесполезное занятие. Почему я так говорю? Да просто потому, что ассемблерный код в десятки раз длиннее того же сишного и писать его все же сложнее. Однако операционная система с драйверами это не все, что нужно для комфортного пользования компьютером. Нужны были прикладные программы, писать которые на ассемблере чистой воды безумие. Для этих целей использовался высокоуровневый язык.

Когда была написана операционная система Unix, остро встал вопрос о портировании ее на различные компьютеры (ассемблерный код был привязан только к тому типу процессора, для которого был написан). И тогда встал вопрос о создании языка, на котором можно писать и операционные системы с драйверами, и прикладные программы. Причем все это должно делаться с одинаковым уровнем сложности. В конечном счете был создан С, а потом и С++.

Так вот, указатели перекочевали в C++ из C. Я уже упомянул тот факт, что драйвера и операционные системы обращаются к регистрам процессора. Конечно, можно это сделать при помощи ассемблерных вставок, разумно включенными в язык, а можно использовать указатели, содержащие адрес этих регистров. Алгоритм тогда сводился к следующему – мы получаем адрес ячейки с данными и меняем значение ячейки ( не адреса!) на нужное. Об этом мы поговорим гораздо позже.

Чтобы не быть голословным, продемонстрирую это на примере. Но для начала давайте разберемся с синтаксисом указателей.

В C, равно как и в C++, указатель задается так:

тип переменной* имя_указателя.

 

Причем звездочка может стоят сразу как после типа, так и перед именем указателя. Это не будет ошибкой. Просто программисты пишут по-разному. Этот пример дань Паскалю. Лично я раньше ставил звездочку перед именем переменной, но потом почему-то стал ставить ее после типа. Важно также ставить префикс перед именем переменной, чтобы знать, что используется указатель. Обычно ставят символ p или ptr, здесь роли не играет. Просто вам самим нужно знать, что это указатель. Еще один важный момент — указатель должен быть именно того же типа, как и переменная, адрес которой вы хотите в него поместить. Представьте себе, что вам нужно купить джинсы 48 размера, а вам настойчиво предлагают 46 или 44. Иными словами, данные просто не влезут в такой указатель.

int a=27;
int* pA=&a; // объявим указатель и поместим в него адрес переменной a
cout<<pA<<endl;// теперь важный момент – при помощи указателя можно поменять значение переменной
*pA=34;
cout<<a<

Давайте теперь немного поясню. Во второй строке я объявил указатель типа int и там же присвоил ему адрес переменной a при помощи оператора изъятия адреса &. Я бы мог написать так:

int a=27;
int* pA;
pA=&a;

Просто тот вариант более удобен. Чтобы показать, что сейчас содержится в памяти, я вывел содержимое указателя. А теперь вернемся к тому замечательному свойству, позволяющему через указатель манипулировать данными в ячейке памяти. Для того, чтобы изменить данные, указатель нужно разыменовать. Для этого перед именем указателя нужно поставить *. Теперь мы можем присвоить любое значение. В переменной a сразу же отразятся эти изменения, для чего я и вывел ее содержимое. Поэкспериментируйте. Забегая далеко вперед, все читерские программы, изменяющие значение переменных в играх, делают это при помощи указателей. Вот такое полезное применение. Но не нужно думать, что на этом использование указателей ограничивается.

Мы знаем, что в функцию для гибкости расчетов удобно передавать параметры. Причем параметры передаются по значению и по ссылке. В уроке про функции я этот момент искусственно опустил, но теперь полностью поясню.

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

Механизм работы подобного параметра прост – вместо копии переменной мы передаем в функцию ее адрес. Это очень экономит время процессора и часто весьма удобно. Конечно, возникает вопрос о том, что программист может нечаянно изменить данные в этом указателе. Чтобы этого избежать, используется классификатор const в параметре:

int somefunction (const int* param)
 {
	 *param=34;
	 return *param + *param;
 }

Пока этот код не берите во внимание. Я сейчас поговорю еще об одной возможности работы с адресом переменной. Речь пойдет здесь о ссылке.

Ссылка – это не более, чем псевдоним переменной. По сути, это тот же адрес. Применяется ссылка при передаче параметра по ссылке. Честно говоря, о другом применении ссылок я даже и не знаю. Если можете привести такой пример, буду признателен.

Естесственно, что ссылка предваряется оператором изъятия адреса &, о котором я уже говорил. Давайте теперь я покажу сигнатуру функции, в которой передается параметр по ссылке:

int func (int& ); //прототип функции
int func (int& param)
{
;
}
//внимание на передачу параметра в main
int a=12;
func (a);

Как видите, принцип почти тот же самый, что и с указателями. Теперь с a можно делать все, что душе угодно. Однако, еще раз оговорюсь, лично я ссылки не использую, так же как и мои коллеги. Достаточно указателей. Для сравнения, вышеупомянутая подпрограмма с использованием указателей будет выглядеть так:

int func (int* ); // прототип

int func (int* param)

{

;

}

// внимание на вызов функции в main

int a=21;

func (&a);

При использовании указателей в параметре мы передаем в качестве параметра адрес переменной a. Это почти тоже самое, что и с ссылкой. Важно просто не забывать ставить амперсанд (&).

Пока на этом урок закончен. В следующем уроке мы рассмотрим более углубленно указатели. А пока отдохните.

<<Предыдущий урок                                                        Следующий урок>>

Яндекс.Метрика