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

Темы -

0. Что такое указатели?
1. Обозначения и определения указателей
2. Некоторые специальные указатели
3. Арифметика указателя

0. Что такое указатели?

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

int digit = 42;

Блок памяти зарезервирован компилятором для хранения значения int. Имя этого блока — digit, а значение, хранящееся в этом блоке, — 42. Теперь, чтобы блок запомнился, ему присваивается адрес или номер местоположения (скажем, 24650).

Значение номера локации для нас не важно, так как это случайное значение. Однако мы можем получить доступ к этому адресу, используя & (амперсанд) или оператор адрес, как показано ниже.

printf("The address of digit = %d.",&digit);
 /* prints "The address of digit = 24650. */

Теперь мы можем получить значение переменной digit по ее адресу, используя другой оператор * (звездочка), называемый косвенным или разыменованием или значением по адресу оператор.

printf("The value of digit = %d.", *(&digit);
 /* prints "The value of digit = 42. */

1. Определение указателя и обозначение

Адрес digit может храниться в другой переменной, известной как переменная-указатель. Синтаксис для сохранения адреса переменной в указателе:

dataType *pointerVariableName = &variableName;

Для нашей переменной digit это можно записать так:

int *addressOfDigit = &digit;

Это можно прочитать так: Указатель на int (целое число) addressOfDigit хранит переменную address of(&) digit.

Несколько моментов для понимания -

1. dataType — Нам нужно сообщить компьютеру, какой тип данных у переменной, адрес которой мы собираемся хранить. Здесь int был типом данных digit.

Это не означает, что addressOfDigit будет хранить значение типа int.

Целочисленный указатель (например, addressOfDigit) может только хранить адреса переменных целочисленного типа.

int variable1;
int variable2;
char variable3;
int *addressOfVariables;

Здесь мы можем присвоить адрес variable1 и variable2 целочисленному указателю addressOfVariables, но не variable3, так как он имеет тип char. Нам понадобится переменная-указатель символа для хранения его адреса.

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

Теперь мы можем использовать нашу переменную-указатель addressOfDigit для вывода адреса и значения digit, как показано ниже:

printf("The address of digit = %d.", addressOfDigit);
 /* prints "The address of digit = 24650." */
printf("The value of digit = %d.", *addressOfDigit);
 /*prints "The value of digit = 42. */

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

Обратите внимание, мы использовали %d в качестве идентификатора формата для addressOfDigit. Ну это не совсем правильно. Правильный идентификатор, который будет использоваться, будет.

При использовании %p адрес отображается в шестнадцатеричном формате. Но адрес памяти может отображаться как в целых, так и в восьмеричных значениях. Тем не менее, поскольку это не совсем правильный способ, отображается предупреждение.

int num = 5;
int *p = #
printf("Address using %%p = %p",p);
printf("Address using %%d = %d",p);
printf("Address using %%o = %o",p);

Вывод в соответствии с компилятором, который я использую:

Address using %p = 000000000061FE00
Address using %d = 6422016
Address using %o = 30377000

Это предупреждение отображается при использовании %d

warning: format '%d' expects argument of type 'int', but argument 2 has type 'int *'

2. Некоторые специальные указатели

1. Дикий указатель

char *alphabetAddress; /* uninitialised or wild pointer */
char alphabet = "a";
alphabetAddress = &alphabet; /* now, not a wild pointer */

Когда мы определили наш указатель символа alphabetAddress, мы не инициализировали его. Такие указатели называются дикими указателями. Они хранят мусорное значение, то есть адрес памяти байта, который, как мы не знаем, зарезервирован или нет (помните int digit = 42;, мы зарезервировали адрес памяти, когда объявили его).

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

2. Нулевой указатель

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

char *alphabetAddress = NULL /* Null pointer */

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

3. Пустой указатель

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

void *pointerVariableName = NULL;

Поскольку они носят очень общий характер, их также называют универсальными указателями.

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

void *pointer = NULL;
int number = 54;
char alphabet = "z";
pointer = &number;
printf("The value of number = ", *pointer); /* Compilation Error */
/* Correct Method */
printf("The value of number = ", *(int *)pointer); /* prints "The value at number = 54" */
pointer = &alphabet;
printf("The value of alphabet = ", *pointer); /* Compilation Error */
printf("The value of alphabet = ", *(char *)pointer); /* prints "The value at alphabet = z */

Точно так же указатель типа void должен быть приведен к типу для выполнения арифметических операций.

Пустые указатели очень полезны в C. Библиотечные функции malloc() и calloc(), которые динамически выделяют память, возвращают пустые указатели. qsort(), встроенная в C функция сортировки, имеет в качестве аргумента функцию, которая сама принимает в качестве аргумента пустые указатели.

4. Висячий указатель

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

main(){
  int *ptr;
  ptr = (int *)malloc(sizeof(int));
  *ptr = 1;
  printf("%d",*ptr); /* prints 1 */
  free(ptr); /* deallocation */
  *ptr = 5;
  printf("%d",*ptr); /* may or may not print 5 */
}

Хотя память была освобождена free(ptr), указатель на целое число ptr по-прежнему указывает на этот незарезервированный адрес памяти.

3. Арифметика указателя

Мы уже знаем, что указатели не похожи ни на какие другие переменные. Они не хранят никакого значения, кроме адреса блоков памяти. Отсюда совершенно ясно, что не все арифметические операции с ними будут верны. Имеет ли смысл умножение или деление двух указателей (имеющих адрес)?

У указателей мало, но чрезвычайно полезных операций.

  1. вы можете присвоить значение одного указателя другому, только если они имеют один и тот же тип (если только они не приведены к типу или один из них не является void *.
int ManU = 1;
int *addressOfManU = &ManU;
int *anotherAddressOfManU = NULL;
anotherAddressOfManU = addressOfManU; /* Valid */
double *wrongAddressOfManU = addressOfManU; /* Invalid */

2. к указателям можно только добавлять или вычитать целые числа.

int myArray = {3,6,9,12,15};
int *pointerToMyArray = &myArray[0];
pointerToMyArray += 3; /* Valid */
pointerToMyArray *= 3; /* Invalid */

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

int number = 5;
 /* Suppose the address of number is 100 */
int *ptr = &number;
int newAddress = ptr + 3;
 /* Same as ptr + 3 * sizeof(int) */

Значение, хранящееся в newAddress, будет не равно 103, а 112.

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

int myArray = {3,6,9,12,15};
int sixthMultiple = 18;
int *pointer1 = &myArray[0];
int *pointer2 = &myArray[1];
int *pointer6 = &sixthMuliple;
 /* Valid Expressions */
if(pointer1 == pointer2)
pointer2 - pointer1;
 /* Invalid Expressions
if(pointer1 == pointer6)
pointer2 - pointer6

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

4. вы можете назначить или сравнить указатель с NULL.

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

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

P.S. — Попробуйте найти, какое значение будет храниться в pointerToMyArray для допустимых операций в 1 и 2, если адрес myArray[0] равен 100.