7. Указатели

7.1. Описание указателей и основные операции, связанные с указателями

Можно много программировать на Паскале, писать на нем достаточно содержательные программы, но не использовать указатели и даже не знать о них. Собственно, при традиционном изучении Паскаля до указателей дело доходит лишь тогда, когда начинают заниматься динамическими структурами данных, которые организуют­ся посредством указателей: списками, деревьями и пр.

При программировании на Си использовать указатели приходится постоянно. И настал наконец момент объяснить, что за "волшебный" знак "&" надо ставить перед именем переменной в функции scanf, как передавать переменные в функции по ссылке (т.е., в терминах Паскаля, "с var") и многое другое.

Итак, память компьютера, как известно, имеет линейную ячеистую структуру, т.е. состоит из ячеек, которые пронумерованы. Все ячейки одинаковые, обычно по 1 байту каждая, Номер ячейки принято называть ее адресом Ячейки памяти нумеруются (адресуются) с нуля. Адрес последней ячейки, вообще говоря, определяется объемов памяти компьютера. Это, на наш взгляд, все, что нужно знать, чтобы представлять себе общую картину. "Продвинутые" дети, безусловно, способны добавить массу подробностей относительно способов адресации в различных операционных системах, но позволять им это в рамках рассмотрения данной темы, на наш взгляд, не следует. Они только запутают всех остальных.

Любая переменная, описанная в программе, имеет адрес в памяти. Если переменная занимает в памяти больше одного байта (например, переменные целого типа занимают обычно два), то адресом переменной считается адрес младшей ячейки.

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

Для хранения адресов переменных имеются переменные специальных типов — указатели. Давайте рассмотрим совсем маленький пример, и все, надеемся, станет яснее.

    #include <stdio.h>
    void main(void)
      { int a;
        
int * p;   /* 1 */
        
p=&a;    /* 2 */
         *p=l;      /* 3 */
         printf("a=%d",a) ;
       }

Строчки, нуждающиеся в комментариях, мы пронумеровали (всего таких строчек три). Итак, в строке /* 1 */ описывается указатель на целую переменную. Этот указатель имеет имя р. Поскольку указатель сам по себе тоже обычная переменная, назвать его можно было как угодно. Описание указателя на переменную данного типа отличается от описания переменной данного типа наличием символа "* " перед именем указателя. Это очень просто, но почему-то авторы некоторых книг по Си создают для читателей проблемы "на пустом месте". Так вот, во-первых, описания int* p, int * p и int *p эквивалентны. Место "*" роли не играет. Во-вторых, символ "*" относится к ближайшей переменной. То есть в записи int *р,а описывается указатель на целое р и обычная целая переменная а. Мы тоже могли бы так написать, но не стали, чтобы сделать описания максимально прозрачными.

В строке / * 2 * / указателю р присваивается адрес переменной а. Операция & возвращает адрес переменной. В строке / * 3 * / по адресу, записанному в переменной р, заносится значение 1. Таким образом, как легко понять, значение 1 присваивается переменной а. Безусловно, в примере продемонстрирован несколько экзотический способ присвоить значение переменной, но зато мы познакомились с двумя основными операциями, связанными с указателями: & — операцией взятия адреса и * — операцией "разыменования" указателя (так ее обычно называют), В заключение приведем пример той же самой последовательности действий на Паскале:

var a:integer;
    
p:^integer;
begin
    р:=@a;
   р^:=1;
   writeln ('а=',а)
end.

7.2. Передача параметров в функции "по ссылке"

Пришла пора окончательно разобраться с тем, как передаются параметры в функции. Выше, когда мы обсуждали этот вопрос, мы утверждали, что в Си параметры в функции всегда передаются по значению. Может быть, мы чего-то недоговаривали? Нет, все верно. Но как же тогда работает, например, функция scanf? Ведь она меняет значение своего параметра (помещает в него значение, прочитанное из потока ввода). Так вот, дело в том, что в Си действительно нет передачи параметров в фикции по ссылке, но можно передавать ссылки на параметры (адреса параметров)! И именно так устроены все функции, которые меняют значения своих параметров, именно так устроена scanf, и точно такой же механизм скрыт в Паскале за "var-параметрами". Только в Паскале этот механизм именно скрыт, а в Си он как на ладони! Итак, приведем пример функции, которая меняет значения своих параметров.

void swap(int *a,int *b)
 
{ int t=*a;
  
*a=*b;
  
*b=t;
}

Разумеется, для того чтобы обменять значения двух целых переменных, функцию swap надо вызывать от их адресов Вот так:

int x,y;

<... >

swap (&x, &y) ;

А как же с передачей массивов в функции? Далее мы рассмотрим и этот вопрос.

TopList