.: [предыдущая | оглавление | следующая] :.

2.11. Указатели

2.11.1. Что такое указатель

Когда переменная объявляется в программе, то записывается ее имя и тип. При этом подразумевается, что переменная будет принимать значения из некоторого множества. На этапе выполнения программы каждой переменной соответствует некоторый блок памяти. Этот блок имеет адрес (целое положительное число, обозначающее номер первого байта данного блока) и размер, который зависит от типа принимаемых значений. Например, для целой переменной выделяется 2 байта. Запишем следующее объявление переменной:

int k;

Рассматривая часть «int» оператора объявления, компилятор будет знать, что для переменной «k» требуется два байта оперативной памяти для хранения целых значений. При трансляции само имя «k» будет записано в таблицу символов, а после трансляции данному имени будет сопоставлен адрес блока памяти, длина которого известна и соответствует заданному типу (для нашего примера это 2 байта). Позже, когда мы запишем

k=2;

Мы ожидаем, что во время выполнения программы целое значение 2 будет записано в блок памяти, который выделен под переменную k. Таким образом, k воспринимается как объект, с которым ассоциируется два значения: одно – это число, которое хранится в выделенном блоке памяти, и второе число - это адрес данного блока памяти. В тех случаях, когда необходимо взять значение объекта k, то говорят «rvalue» (правое значение, это означает, что k стоит справа от знака присвоения «=»). В тех случаях, когда мы хотим присвоить значение k, то говорят о «lvalue» (левое значение, это означает, что k стоит слева от знака присвоения «=»).

Таким образом /Керниган и Ритчи/:

  1. объект есть поименованная область памяти, имеющая адрес и размер;
  2. rvalue объекта - это значение, которое хранится в объекте;
  3. lvalue объекта - это выражение, ссылающееся на объект (адрес объекта).
Приведем следующий текст фрагмента программы:
int k, j; //1
k=2; //2
j=7; //3
k=j; //4

В строке 3 для переменной j берется lvalue, и присваивается значение 7. В строке 4 для j берется rvalue (ее значение 7) и присваивается переменной k, для k берется lvalue (ее адрес, куда будет записано значение 7).

Теперь нам бы хотелось иметь переменную, значением которой был бы адрес некоторого объекта (иными словами, lvalue некоторого объекта). Такой переменной стала переменная типа указатель или просто указатель. Указатель объявляется с помощью звездочки, например:

int * ptr;

В этом примере ptr - это имя переменной (такое, как “k” в предыдущем примере); Символ “*” говорит о том, что переменная ptr - указатель, т.е. значение данной переменной есть адрес некоторого блока ОП. Ключевое слово int сообщает компилятору о том, что наша переменная содержит адрес блока памяти, в котором будет храниться целое число. При этом компилятор заранее знает, какой объем памяти будет занимать указатель (это зависит от процессора и системы программирования (см. модель памяти компилятора)). Таким образом, можно сказать, что указатель установлен на целое (int). Однако когда мы записываем int k; мы не присваиваем значения переменной k. Если это объявление сделано вне какой либо функции, то компилятор инициирует ее в ноль. В противном случае k может иметь любое значение.

В нашем примере переменная ptr не имеет значения, это означает, что указатель ptr не содержит адреса на блок памяти, где записано целое число. В случае, если указатель объявлен вне тела какой либо функции, ему присваивается специальное значение, которое называется null-указатель. Это значение гарантирует, что указатель, имеющий данное значение, не ссылается ни на какой-либо объект или функцию.

В действительности реальное значение null не обязательно должно равняться нулю. Все зависит от специфики компьютера и операционной системы. Для того, чтобы сделать Си-программы совместимыми между разными типами компьютеров, ввели специальный макрос для представления null-указателя. Этот макрос имеет имя NULL. Таким образом, оператор присваивания ptr=NULL; гарантирует, что указатель ptr примет null значение. Для проверки значения мы можем написать следующий условный оператор

if (ptr==NULL) {...}

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

ptr=&k;

Операция & обеспечивает определение lvalue переменной k в случае, если k стоит справа от знака присвоения “=”. После выполнения оператора присвоения говорят, что ptr указывает на объект k.

Для того, чтобы присвоить значение объекту через указатель, используют специальную операцию унарная звездочка “*”. Рассмотрим следующий пример:

*ptr=7;

При выполнении этого оператора значение 7 будет записано в блок памяти, адрес которого записан в указателе. Таким образом, если ptr указывает на k (адрес переменной k хранится в ptr), то переменной k будет присвоено значение 7. В этом примере операция “*” означает, что lvalue вычисляется с помощью указателя ptr. Теперь, если мы запишем

j=*ptr;
,то переменной j будет присвоено значение 7. В данном случае *ptr означает rvalue, т.е. будет взято значение объекта, адрес которого хранится в указателе ptr. Ниже приведен простейший пример для пояснения идеи указателя.
#include <stdio.h>
int j,k;
int *ptr;
int main()
{
      j=1;
      k=2;
      ptr=&k;
      printf("\n ++++++
Пример №1 +++++++\n");
     
printf("j имеет значение %d и хранится по адресу %p\n", j,(void *)&j);
      printf("k имеет значение %d и хранится по адресу %p\n", k,(void *)&k);
      printf("ptr имеет значение %p и хранится по адресу %p\n", ptr,(void *)&ptr);
      printf("значение целого, на которое указывает ptr равно %d\n" ,*ptr);
      return 0;
}

2.11.2. Указатели на массивы

Указатели можно организовать не только на объекты, такие как char a или int c, но и на массивы. Например, это можно, используя ключевое слово typedef, которое предназначено для создания нового типа. Например:

typedef int Array[10];

При таком объявлении идентификатор Array становится новым типом данных. Тогда можно записать:

Array my_arr; /* массив из 10 целых чисел */

или

Array arr2d[5]; /* 5 массивов, каждый из 10 целых чисел */

Можно также организовать указатель:

Array * p1d;

Тогда следующие присваивания будут корректны:

p1d=&my_arr;
p1d=&arr2d[0];
p1d=arr2d;

Важно также отметить, что sizeof(Array) будет равен 10*sizeof(int). Поэтому, если мы будем делать инкркмент (p1d+1), то адрес увеличится на 10*sizeof(int) байт. Можно объявить указатель на массив, не используя typedef. Это достигается с помощью следующей синтаксической конструкции:

int (*aptr)[10]; /*объявление указателя на массив из 10 целых*/

Не надо путать это объявление с похожим на него:

int * aptr[10];

В этом объявлении записан массив указателей типа int.

2.11.3. Указатели и динамическое распределение памяти

Динамическое распределение памяти является эффективным средством программирования и используется тогда, когда размер объекта заранее вычислить нельзя или неэффективно. Например: размер матрицы заранее не известен, а вычисляется в процессе выполнения программы. Динамическое распределение памяти осуществляется с помощью специальных функций, на вход которых подается размер требуемой памяти, а на выходе адрес блока памяти. В некоторых системах программирования такие функции называются malloc() или calloc(), прототипы которых описаны в файлах alloc.h или stdlib.h. Необходимо также отметить, что эти функции возвращают адрес типа void, поскольку они заранее не знают тип информации, который будет храниться в запрашиваемом блоке памяти. Организация динамического распределения памяти в Си без указателей невозможна. Рассмотрим простой пример динамического распределения памяти под массив целых чисел:

int * iptr;
iptr=(int *)malloc(10*sizeof(int);
if (iptr==NULL) { /*объявление указателя на массив из 10 целых*/ }

Если запрашиваемый блок памяти по какой-либо причине не может быть выделен, то функция выделения памяти malloc() возвращает нулевое значение указателя NULL. Поэтому после вызова malloc() необходимо проверить значение указателя, и если оно равно NULL, то записать код для обработки данного события (обычно выводят сообщение о нехватки памяти и программа на этом завершается).

Если память выделена, то далее этот блок используют по назначению. В нашем примере можно предложить следующий код:

int k;
for (k=0; k<10; k++) iptr[k]=10-k;

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

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

int *ptr_copy=iptr;
for (k=0; k<10; k++) *ptr_copy++=10-k;

Возврат выделенной памяти производится с помощью вызова функции free(). Для нашего примера будет записано: free(iptr):

#include <stdio.h>
#include <alloc.h>
//typedef int Ra[5];
//Ra *ptr;
int (*ptr)[5]; //указатель на массив из 5 элементов
main()
{
      int i,j,k=0;
      ptr=(int (*)[5])malloc(5*10*sizeof(int));
      for(i=0; i<10; i++)
            for(j=0; j<5; j++) ptr[i][j]=k++;
                  for(i=0; i<10; i++)
                  {
                        printf("\n");
                        for(j=0; j<5; j++)
                        {
                             printf("%d ",ptr[i][j]);
                        }
                  }
}

2.11.4. Указатели на структуры

Рассмотрим вопросы организации указателей на объекты - структуры. Предположим, что задана структура:

struct Student
{
      char Name[20];
      char Group[20];
      char BirthDay[20];
      int YearsOld;
};

Предположим также, что объявлен объект и указатель указанного типа:

struct Student MyFriend={“Nik”, “5566-1”,“1 may“, 17};
struct Student *ptr;
тогда
ptr=&MyFriend; //присвоить адрес объекта указателю
printf(“%s”,ptr->Name); //печать элемента структуры через указатель, здесь стрелка
//означает, что берется значение элемента структуры через адрес, записанный в ptr.
printf(“%s”,ptr->BirthDay);
ptr->YearsOld++;

Рассмотрим случай, когда в объекте типа структура имеется указатель:

struct Vendor //описание производителя
{
      char Name[10];
      char Addres[20];
}
struct Goods //описание товара
{
      struct Vendor *ven;
      char Name[20];
      char Price;
};
struct Vendor vendor={ “фабрика”, “Луна”};
struct Goods Mouse, *ptr;
ptr=&Mouse;
ptr->ven=vendor;
strcpy(ptr->ven->Name,”Завод”) ;

Если в структуре Goods переменную ven заменить на массив ven[20], то к соответствующим полям структуры Vendor можно сослаться следующим образом:

printf( ptr->ven[k]->Name );

В структурах можно вводить рекурсивные описания, например для записи поля списка:

struct Link {
   struct Link * next; //указатель на следующий элемент списка
   int type; //можно хранить тип данного (это зависит от программиста)
   void * ptr; //указатель на данные, тип void можно преобразовать к требуемому
};

При объявлении struct Link *item; и если ему будет присвоено конкретное значение адреса начала списка такой структуры, то можно записывать:

item->next; //адрес следующего элемента
item->next->next; //адрес второго элемента
item->next->next->next; //адрес третьего и т.д.
item->type; //тип данного элемента
item->next->type; //тип следующего элемента списка
item->ptr; //указатель на данные элемента списка
item->next->ptr; //указатель на данные следующего элемента

2.11.5. Указатели на функции

В Си можно организовать указатель на функцию. Правила записи здесь следующие: <описание типы выхода> (*<имя>) (<список параметров>), например:

void (*func)(int x, int y); //объявить указатель func

Простейший пример, показывающий работоспособность, следующий:

void (*f)(int i); //указатель на функцию
void f1(int i) { printf(“%d <<---\n", 2*I); }
void f2(int i) { printf(“%d <<+++\n", 3*I); }
void main()
{
      f=f1; //присвоить указателю адрес первой функции и вызвать ее
      f(10);//через указатель f c аргументом 10
      f=f2;//присвоить указателю адрес первой функции и вызвать ее
      f(20); //через указатель f c аргументом 20
}

Возможно использование указателя как параметр в функции, например:

void ForEach(int massiv[],int n, void (*func)(int el))
{
      int i;
      for(i=0;i<n; i++) func(massiv[i]);
}

В этом примере описан прием программирования, при котором организуется просмотр элементов заранее не определенной функцией, она в этом случае воспринимается как параметр, который передается извне, при вызове. Например:

void Show1(int x) { printf(“вариант печати №1 %d”,x);}
void Show2(int x) { printf(“вариант печати №2 %d”,x);}
int y[5] ={ 1, 3, 7, 9, 11};
ForEach(y,5,Show1); //вывод массива по первому варианту
ForEach(y,5,Show2); //вывод массива по второму варианту

2.11.6. Указатели и константы

Указатель или объект, на который указывает указатель, могут быть объявлены как константы (это делается с помощью модификатора const). Рассмотрим несколько примеров:

int i; //i - целая переменная
int * pi; //неинициализированный указатель на int
int * const cp=&i; //указатель константа на переменную i
const int ci=7; //целая константа
const int * pci; //не инициализированный указатель на целую константу
const int * const cpc=&ci; //константа указательна целую константу

Следующие присвоения являются правильными:

i=ci; //целой переменной присвоено значение константы
*cp=ci; //присваивается значение константы по указателю-константе обычному объекту i (см.выше)
++pci; //инкремент указателя на константу
pci=cpc; //присвоение указателю на константу значение константы указателя на константу объект

Следующие присвоения являются неправильными:

ci=0; //нельзя присваивать значение объекту-константе
ci++; //нельзя изменять значение объекта-константы
*pci=3; //нельзя изменять значение объекта-константы через указатель
cp=&ci; //нельзя изменять адрес указателя-константы
cpc++; //нельзя изменять значение указателя-константы
.: [предыдущая | оглавление | следующая] :.