Переменная — это участок памяти, к которому мы обращаемся по идентификатору, то есть имени переменной. У этой области есть уникальный адрес, выраженный числовым значением, понятным для компьютера. Адрес можно сохранить в специальной переменной, именуемой указателем.

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

Указатели тесно связаны с динамическими типами данных. Во время компиляции программы выделяются участки памяти под переменные, их размер неизменен, но содержимое может изменяться. Именно указатели позволяют управлять новыми участками памяти на этапе выполнения программы. Динамические типы данных будут рассмотрены позже.

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

  int i = 0;
  printf ("i=%d, &i=%p \n", i, &i);

Результат этого кода выводит приблизительно следующее (ваше шестнадцатеричное значение адреса будет отличаться):

i=0, &i=0x7fffa40c5fac 

Символ амперсанда (&) перед переменной извлекает ее адрес. Формат %p используется для вывода адреса на экран. Адреса обычных переменных неизменны во время работы программы, в чем можно убедиться:

  int a = 6;
  float b = 10.11;
  char c = 'k';
  printf("%d - %p, %.2f - %p, %c - %p\n", a,&a, b,&b, c,&c);
 
  a = 2; b = 50.99; c = 'z';
  printf("%d - %p, %.2f - %p, %c - %p\n", a,&a, b,&b, c,&c);

Результат:

span>6 - 0x7fff8e1d38e4, 10.11 - 0x7fff8e1d38e8, k - 0x7fff8e1d38ef
2 - 0x7fff8e1d38e4, 50.99 - 0x7fff8e1d38e8, z - 0x7fff8e1d38ef

Изменение значений переменных не влияет на их адреса памяти.

Зная адрес, можно извлечь значение, находящееся там, используя * перед адресом:

  int a = 8;
  printf("%d \n", *&a);

На экране будет показано число 8.

Запись &a не всегда удобна. Поэтому введены указатели, которые сохраняют адрес области памяти.

Указатели в языке C являются типизированными: при их объявлении указывают тип. Позже мы изучим, как с указателями выполняются арифметические операции, и насколько важно их точное определение. Чтобы объявить указатель, нужно поставить * перед его именем. Пример:

  int *pi;
  float *pf;

Обратите внимание, * при объявлении обозначает указатель; в выражениях — извлечение значения по адресу указателя. Рассмотрим код:

  int x = 1, y, z = 3;
  int *p, *q;
 
  p = &x;
  printf("%d\n", *p); // 1
 
  y = *p;
  printf("%d\n", y); // 1
 
  *p = 0;
  printf("%d %d\n", x, y); // 0 1
 
  q = &z;
  printf("%d\n", *q); // 3
 
  p = q;
  *p = 4;
  printf("%d\n", z); // 4
 
  printf("%p %p\n", p, q); // p == q

Комментарии показывают текущие состояния памяти. Подробно описываем происходящее:

  1. Под пять переменных выделяется память: три для int переменных и два для указателей. В x и z записаны 1 и 3 соответственно.
  2. Указатель p получает адрес x. Извлекая значение (*p), получаем 1.
  3. y присваивается значение из ячейки, на которую указывает p. Обе области памяти (x и y) содержат единицы.
  4. По адресу p записывается 0, меняя x. Поскольку p указывает на x, y не затрагивается.
  5. Указатель q получает адрес z. Извлекая значение (*q), получаем 3.
  6. p получает значение из q, что приводит к ссылке p и q на z.
  7. По адресу p пишем 4, меняя z.  P, как указатель на z, изменяет и его значение.
  8. Убедимся, что p и q указывает на тот же участок памяти.

Для хранения адреса указателя также должна быть выделена память, что можно определить функцией sizeof():

  int *pi;
  float *pf;
 
  printf("%lu\n", sizeof(pi));
  printf("%lu\n", sizeof(pf));

Для всех указателей выделяется одинаковое количество памяти. Это связано с тем, что размер адреса обусловлен системой, а не типом данных. Зачем же указывать тип? Тип данных определяет размеры и диапазоны памяти, на которые ссылается указатель, а также вычисления между областью памяти и указателем.

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

  int *pa, *pb;
  float *pc;
 
  printf(" %p %p %p\n", pa, pc, pb);
  printf(" %d %f\n", *pa, *pc); // может возникнуть ошибка

Результат (в Ubuntu):

 0x400410 0x7fff5b729580 (nil)
 -1991643855 0.000000

Использование неопределенных указателей может привести к серьезным ошибкам. Чтобы этого избежать, присваивают значение NULL, говорящее о том, что указатель ни на что не указывает. Пока ему не установят конкретное значение, его использовать нельзя:

  int a = 5, b = 7;
  float c = 6.98;
  int *pa, *pb;
  float *pc;
 
  pa = pb = NULL;
  pc = NULL;
 
  printf(" %15p %15p %15p\n", pa, pc, pb);
  // printf(" %15d %15f %15d\n", *pa, *pc, *pb); // Error
 
  pa = &a;
  pb = &b;
  pc = &c;
 
  printf(" %15p %15p %15p\n", pa, pc, pb);
  printf(" %15d %15f %15d\n", *pa, *pc, *pb);

Результат (в Ubuntu):

           (nil)           (nil)           (nil)
  0x7fffeabf4e44  0x7fffeabf4e4c  0x7fffeabf4e48
               5        6.980000               7 

Если извлечь значение по неназначенному указателю, возникает "ошибка сегментирования".

На занятии важными концепциями являются: адрес переменной (&var), указатель определенной переменной (type *p_var; p_var = &var), получение значения по адресному указателю (*p_var). Если это вызывает некоторое недоумение, не переживайте. Значимость указателей раскроется позже с изучением новых тем.

Задание:

Продемонстрируйте все примеры из данного урока на практике и придумайте собственные примеры использования указателей.

Вопросы для самопроверки:

  1. Как извлечь адрес переменной в языке C?
  2. Что произойдет при изменении значения обычной переменной с точки зрения памяти?
  3. Какие типы данных связаны с указателями и почему?
  4. Как объявить переменную-указатель?
  5. В чем заключается разница между обозначением указателя при его объявлении и в выражениях?
  6. Что происходит при присваивании NULL указателю?
  7. Почему использование неинициализированных указателей может привести к ошибкам?

Программа курса:

  1. Описание курса
  2. Введение в язык программирования C
  3. Типы данных в C и форматированный вывод
  4. Символьные типы и управляющие символы в C
  5. Операторы ветвления и switch в C
  6. Циклы и операторы в языке C
  7. Битовые операции в языке C
  8. Посимвольный ввод и вывод в C - буферизация
  9. Переменные, адреса и указатели в C
  10. Передача аргументов по ссылке и значению в C
  11. Форматированный ввод данных с использованием scanf
  12. Генерация псевдослучайных чисел на C
  13. Адресная арифметика в массивах C
  14. Передача массивов в функции и указатели
  15. Строки в языке C - особенности и функции работы
  16. Функции работы со строками в C
  17. Работа со структурами в C - создание и применение
  18. Динамические структуры данных в C
  19. Ввод и вывод данных из файлов в языке C
  20. Передача аргументов в C и работа с файлами
  21. Препроцессор в языке C - директивы и макросы
  22. Создание и компиляция многофайловых программ в C
  23. Использование статических и динамических библиотек в C