Лекция 14. Указатели (продолжение)

Указатели

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

int a, b;
int *p1, *p2; // указатели на числа типа int.

// p1 и p2 -- неинициализированные указатели.

p1 = &a; // p1 указывает на a.
p2 = &b; // p2 на b.
*p1 = 2;
*p2 = 3;

printf("a = %d, b = %d, *p1 = %d, *p2 = %d\n", a, b,
         *p1, *p2);
// a = 2, b = 3, *p1 = 2, *p2 = 3
// см. рисунок 1

p1 = p2; // Присвоить адресу p1 адрес указателя p2.
         // Теперь и p1, и p2 указывают на b.
p1 = *p1 + 10;

printf("a = %d, b = %d\n", a, b);
// a = 2, b = 13
// см. рисунок 2
_images/example1.png

Оператор &

int x;
int arr[10];

int *p = &x;
*p = 2;

p = &arr[0];
*p = 5;

p = &arr[4];
*p = 8;
// Значения переменных показаны на рис. 3
_images/example2.png

Вычисляет адрес переменной или другого места в памяти, к которому применяется. Например: &x, &a[i].

&(a + b) – ошибка. Временные промежуточные выражения не имеют постоянного адреса и к ним нельзя применять оператор &.

Если хотите указатель на какую-то сумму или другое временное выражение, сначала сохраните его в переменную, потом создавайте указатель на неё.

Оператор & добавляет указатель к тому типу, к которому применяется. Если x имеет тип int, то &x имеет тип (int *). Если x имеет тип double, то &x имеет тип (double *).

Оператор *

Если p – это указатель, то *p - это значение, на которое указывает p.

Если p – это (int *), то *p – это int. Если p – это (double *), то *p – это double.

* и & – взаимно обратные операции, то есть:

*&x == x  // верно для любого x, у которого есть место
          //  в памяти
&*p == p  // верно для любого p, который указывает
          //  на доступную ячейку памяти

Приведения типов указателей

Размеры всех типов указателей: double *, int *, char * одинаковы и зависят архитектуры процессора, для которой компилируется программа. На 32-битный процессорах размер указателя 4 байта, на 64-битных уже 8 байт.

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

int x;
int *pi = &x;
// Заметьте: звёздочка в предыдущей строке -- это часть
// типа int *, а не оператор! То есть, это сокращённая форма записи
//     int *pi;
//     pi = &x;

char *pc = (char *) &x;  // Чтобы компилятор не ругался, что
// слева от присваивания тип char *, а справа int *, мы делаем
// приведение типа вручную с помощью записи (char *)

Кроме указателей на все известные нам типы данных, в Си есть особый тип: void * (нетипизированный указатель).

Типа void не существует, поэтому оператор * к указателям типа (void *) применять нельзя. Нетипизированный указатель применяется для работы с памятью, тип которой неизвестен.

int x;
void *pv = &x; // К типу void * любой указатель
        // преобразуется автоматически

Нулевой указатель (NULL)

NULL – это константа 0, применяемая для обозначения нулевой ячейки памяти.

NULL используется для обозначения некорректной ячейки памяти.

Свойства:

  1. NULL логически ложен.
  2. В прикладных программах чтение или запись в нулевую ячейку памяти приводит к ошибке доступа, так как нулевая ячейка памяти занята ОС.
  3. NULL автоматически приводится к указателю любого типа без явного преобразования типа.

NULL используется для инициализации указателей, если сразу правильно инициализировать нельзя

int *p = NULL;
// Какие-то действия
*p = 123; // Если p по-прежнему NULL,
        // ошибка памяти гарантирована.

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

int *p;
int arr[4] = {10, 20, 30, 40};
p = &arr[0];
*p = 123;
printf("p = %p, *p = %d\n", p, *p);
// p = 0x12345678, *p = 123
*(p + 1) = 456;
printf("p + 1 = %p, *(p + 1) = %d\n", (p + 1), *(p + 1));
// p + 1= 0x1234567C, *(p + 1) = 456
*(p + i)
сначала вычисляется сумма (p_в_байтах + i * sizeof(тип данных, на который указывает p)) затем получившееся число используется как адрес ячейки, из которой оператор * извлекает значение. Такое поведение выбрано, чтобы облегчить доступ к элементам массивов.

Страшная правда о том, как на самом деле работают массивы:

a[i]             <==============>          \*(a + i)
Удобный синтаксис                     Как на самом деле

Рекомендации по синтаксису:

p[i] – когда указатель указывает на много значений

*p – когда указатель указывает на одно значение

int arr[10];           <====>         int arr[10];
int *p = &arr[0];                     int *p = arr;
                                Имя массива -- указатель
                                на его нулевой элемент

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

size_t strlen(char *s)
{
    size_t i;
    for (i = 0; s[i] != '\0'; ++i) {}
    return i;
}
size_t strlen(char *s)
{
    char *s0 = s;
    for ( ; *s != '\0'; ++s) {}
    return s - s0;
}

Инкремент и декремент в выражениях с оператром *

Инкремент воздействует на указатель:

*p++
сначала использовать *p, затем p++
*++p
сначала ++p, затем использовать *p

Инкремент воздействует на значение:

(*p)++
сначала использовать *p, потом (*p)++
++*p
сначала ++(*p), потом использовать *p