Это, пожалуй, самая сложная тема для людей, начинающих программировать на C. Однако так быть не должно.

Это потому, что указатели похожи на другие переменные с некоторыми дополнительными функциями. По сути, указатели - это просто переменные, в которых хранятся «адреса».

Начнем с основ.

Объявление переменной с инициализацией выглядит примерно так:

int a = 5;

Это инструкция компилятору выделить блок памяти, достаточный для хранения целочисленной переменной, и присвоить ей значение 5;

Ключевой момент: «выделить блок памяти».

Допустим, если для конкретного компилятора и ОС размер целого числа составляет 4 байта (что, кстати, чаще всего). Следовательно, для целого числа a выделяется 4 байта памяти;

Поскольку будет так много распределений / освобождений переменных, должна быть соответствующая система для управления и работы со всем этим.

Следовательно, каждая точная точка (читай: каждый байт) в памяти имеет адрес. Давайте посмотрим на диаграммы ниже, чтобы понять концепцию распределения памяти.

Допустим, для выделения a компилятор нашел свободное место в 4 байта, начиная с адреса 250. Таким образом, теперь он резервирует 4 байта памяти, начиная с 250, для идентификатора a.

Итак, есть целое число (занимающее 4 байта), имеющее «адрес» как 250, и этот блок из 4 байтов будет называться с использованием идентификатора как a.

Уф, это было долго и, вероятно, немного избыточно, но вся эта штука является сутью всего, что связано с указателями (хотя я еще даже не говорил об указателях!)

Наконец-то поговорим об указателях.

«Указатель» - это просто переменная, в которой хранится адрес. Адреса - это в основном целые числа. Таким образом, можно с уверенностью сказать, что:

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

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

int a;

Приведенный выше оператор фактически создает целое число в ячейке памяти, начиная с 250 и занимая 4 байта.

Давайте теперь рассмотрим пример объявления указателя.

int *ptr;

Этот оператор похож на обычное целочисленное объявление.
Компилятор пытается понять смысл этого утверждения, читая объявление как:

Объявление переменной, в которой хранится * типа int и имеет идентификатор «ptr»

Прочтите это как:

Объявление переменной, в которой хранится адрес целого числа и имеет идентификатор «ptr».

Поскольку адрес в основном является целым числом, компилятор пытается найти 4 свободных
(еще не выделенных ни для чего другого ИЛИ нераспределенного) байта памяти и связывает их с идентификатором 'ptr '.

Допустим, указатель получает адрес 820. В совокупности _10 _, _ 11 _, _ 12_ и 823 будут хранить одно целое значение, которое фактически будет адресом какой-то другой переменной.

В C, если вы только объявляете переменную без какой-либо инициализации

int a;
int b;
int *ptr;

в конечном итоге вы ничего не делаете с данными, которые ранее хранились бы в этой последовательности байтов.

820–823 может ранее иметь любое значение, и компилятор C не обязательно должен беспокоиться о присвоении ему значения, если это специально не запрашивается. В таких условиях «неинициализированных переменных» могут произойти непредвиденные вещи, которые вы, возможно, не учли.

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

Правило большого пальца: всегда инициализируйте переменную при ее объявлении!

// Correct way to declare and initialize variables:
int a = 0;
int b = 0;
int *ptr = NULL;

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

(Примечание: всякий раз, когда вы читаете «память», думайте о RAM, а не о диске или любой другой постоянной (постоянство хранимых данных) форме хранения. Все задачи на компьютере выполняются путем копирования данных в ОЗУ, а затем фактического выполнения операций оттуда.

Выполнение операций с постоянным хранилищем будет слишком медленным и обременительным, поскольку скорости чтения / записи ОЗУ будут намного выше, чем скорости чтения и записи постоянного хранилища)

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

int * ptr = &a;

Читается как: выделить 4 байта в памяти (для идентификатора «ptr») для хранения адреса целого числа и присвоить ему значение, которое является адресом идентификатора «a».

Оператор & в C означает «адрес»

Переменная a имеет адрес 250 (я знаю, что 4 байта в совокупности хранят любое предполагаемое значение, но начального адреса и его длины также достаточно, чтобы узнать, сколько байтов нужно прочитать.) Следовательно, адрес любой переменной, как правило, обозначается адресом первого байта, который он занимает в памяти.

(Другое примечание: когда я говорю, что 4 байта кумулятивно хранят целое число,
это означает, что: 4 байта = 4 x 8 = 32 бита.

32 бита будут хранить значение в совокупности. Поскольку
5 преобразовано в двоичное значение: 101
или просто 5 с основанием 10 = 101 с основанием 2.

101 хранится в крайних правых 3 битах переменной, начиная с 250, и все биты, идущие до нее, устанавливаются в ноль (0).

Другие примеры (при условии, что b и c имеют адреса 375 и 405 соответственно):

int b = 142; // 10001111
int c = 32756; // 111111111110100

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

Возвращаясь к указателям, давайте посмотрим на объявление указателя (с инициализацией ниже)

int * ptr = &a;

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

Чтобы ответить на этот вопрос, нам сначала нужно знать, что мы можем делать с адресом.

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

Вот тут-то и пригодится оператор *.

*, происходящее на стороне выражения оператора объявления, означает, что выделение памяти предназначено для переменной-указателя. (например: int *ptr;)

*, встречающийся в любом другом выражении, в основном: «значение, хранящееся по адресу»

printf("%d", *ptr); // result is : 5

Объяснение:
*ptr интерпретируется как
'Значение, хранящееся в __адрес, который хранится в ptr__ '
ИЛИ
' Значение, хранящееся в __адресе 250__ '

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

int b = 6;
int *ptr2 = &b;
*ptr2 = 15;
printf("%d %d",b,*ptr); // 15 15

Внимательно посмотрите на строку номер 3 здесь, *ptr2 = 15;
Мы пытаемся понять утверждение как «присвоить 15 значению по адресу, хранящемуся в ptr2». Поэтому он пытается изменить значение в месте, зарезервированном для b. Естественно, значение b изменится на 15, потому что b сам по себе является просто идентификатором, а фактическое значение сохраняется в конкретном месте в памяти.

Надеюсь, теперь мы научились использовать оператор * в объявлениях и с обеих сторон оператора присваивания!

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

Дело в том, что что-то вроде

int y = *ptr;

компилятору предлагается оценить RHS как
«значение, хранящееся по адресу, который хранится в ptr»

Чтобы прочитать значение, начинающееся с 250, компилятор должен знать, сколько байтов нужно прочитать!

4 байта? 1 байт? 10 байт?

скажем, мы пишем:

double x = 4.9;
int* ptr_x = &x;
int y = *ptr_x;

double занимает 8 байт памяти (опять же, в зависимости от компилятора и ОС).

Допустим, значение 4.9 хранится в 8 байтах, начиная, скажем, с 670.
«ptr_x» просто должен сохранить адрес, и он хранит адрес 670 в себе.

Когда вы выполняете * ptr_x на RHS (строка 3 выше), компилятор начинает чтение значения, хранящегося в ячейке STARTING с 670. Теперь ему нужно знать, сколько байтов нужно прочитать, чтобы фактически получить значение 4.9;
Ему необходимо знать: «прочитать 8 байтов, начиная с адреса 670»

Если бы он прочитал всего 4 байта, это были бы только байты 670–673, что было бы неполной и неверной информацией, что произойдет, если вы сохраните адрес двойного числа в целом числе (вы потеряете информацию!)

Это может привести к неожиданному поведению и, следовательно, не допускается компилятором.

Правило большого пальца: вы можете назначить адрес переменной только указателю на тот же тип данных (просто чтобы он знал, сколько байтов нужно прочитать!)

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

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

int a = 5;
int * ptr = &a;
int x = *ptr;

Если да, то отлично! Давайте перейдем к следующей статье, иначе вернемся и дадим эту статью еще одному пациенту, прочитанному с самого начала, и, надеюсь, все прояснится.