Адресные пространства

Для лучшего понимания концепции указателей желательно изучить понятие адресного пространства и трансляции адресов, хотя бы в первом приближении. Это достаточно обшираная тема, которую не возможно объяснить парой параграфов, поэтому я рекомендую обратиться к учебнику "Computer Systems: A Programmer's Perspective", где она изложена на первых страницах главы 9, "Virtual Memory".

Указатель это адрес

Указатель это целое число, которое представляет из себя адрес некоторой ячейки памяти.

Для чего используют указатели

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

Объявление указателей

Чаще всего указатели объявляются следущим образом:

type * name;
// или так:
type *name;

Где type это тип данных, которые будут храниться в ячейке памяти, адрес которой будет присвоен этому указателю, а name это имя самого указателя. Пробел между "*" и "name" опционален.

Примеры:

int * my_number;	// указатель на целое число
char * my_character;	// указатель на символ
double * my_float;	// указатель на число с плавающей точкой двойной точности

int * a, * b;	// 2 указателя на целое число
int *a, *b;	// 2 указателя на целое число, эквивалентно предыдущей строке

Позиция оператора * при объявлении указателей часто вызывает вопросы. При объявлении одной переменной указателя она не имеет значения. Таким образом следущие выражения эквивалентны:

int *p;
int* p;
int * p;

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

int a, *b;	// a - целое число, b - указатель на целое число
int* x, y, z;	// x - указатель на целое число, y и z целые числа

Более подробно об этом можно почитать здесь.

Разыменование указателей

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

Рассмотрим следующую программу:

#include <stdio.h>

int main(int argc, char** argv){
  // Oбъявим 4 переменных.
  // a и b - целые числа, а pa и pb - указатели на целые числа
  int a, b, *pa, *pb;

  // проинициализируем а и b значениями 1 и 2 соответсвенно
  a=1;
  b=2;

  // проициализируем наши указатели адресами переменных а, b: 
  pa=&a; // выражение "&a" обозначает "адрес переменой а"
  pb=&b;

  // объявим еще один указатель p, проинициализируем его
  // адресом переменной a сразу же при объявлении
  int* p = &a;

  // теперь, путем разыменования указателей pa и pb выведем на экран
  // адреса ячеек памяти, на которые ссылаются эти указатели
  // и целые числа, хранящиеся в этих ячейках
  printf("a is %d, located at %p\n", *pa, pa);
  printf("b is %d, located at %p\n", *pb, pb);  
  printf("pointer p points to %d (a), located at %p\n", *p, p);

  // теперь запишем в ячейку памяти, адрес которой хранится в указателе p
  // целое число 7 и выведем результаты на экран
  *p = 7;
  printf("a is %d, located at %p\n", *pa, pa);
  printf("pointer p points to %d (a), located at %p\n", *p, p);
}

Стоит обратить внимание на синтаксис который используется в этой программе при инициализации указателя сразу при его объявлении:

int* p = &a;

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

int * p = &a;
int *p = &a;

Массивы и указатели

Рассмотрим следующую простую программу, которая использует указатели для доступа к элементам массива:

#include <stdio.h>
int main(int argc, char** argv){
  // создадим массив символов (строку)
  char* str = "abcde";

  // теперь выведем на экран первые 3 элемента этого массива и их адреса

  // str - указатель на первый элемент массива, то есть его адрес
  // таким образом, при разыменовании *str мы получим значение первого элемента массива
  printf("str[0] = %c located at %p\n", *str, str);

  // Далее, так как массив это упорядоченный набор ячеек одинакового размера, и мы знаем адрес его
  // первой ячейки, мы можем прибавить к нему размер ячейки, чтобы получить адрес следующей ячейки.
  // Так как каждая ячейка нашего массива содержит символ (char), ее размер можно определить
  // выражением sizeof(char). Таким образом, выражение str + sizeof(char) возвращает адрес второго
  // элемента массива, а произведя разыменование *(str + sizeof(char)) мы получаем значение, которое
  // находится во второй ячейке массива.
  printf("str[1] = %c located at %p\n", *(str + sizeof(char)), str+sizeof(char));

  // По аналогии с предыдущей операцией, получим адрес и значение третьего элемента массива. В этом
  // случае требуется дважды прибавить размер ячейки к адресу первого элемента.
  printf("str[2] = %c located at %p\n", *(str + 2 * sizeof(char)), str+2 * sizeof(char));
}

Created: 2021-11-07 Вс 16:22

Validate