Лекция 13. Указатели, начало

Операторы префиксного и постфиксного инкремента и декремента

i++
оператор постфиксного инкремента
++i
оператор префиксного инкремента

Оператор --, соответственно, называется декрементом. Отличия префиксных и постфиксных операторов заключаются в том, когда именно производится изменение переменной:

i++
“использовать i в выражении, затем увеличить i на 1”
++i
“увеличить i на 1, затем использовать уже увеличенное i в выражении”

Пример использования операторов инкремента:

int a = 0;
int b, c;
b = a++ * 2; // Теперь b == 0, a == 1
c = ++a * 2; // Теперь c == 4, a == 2

Если инкремент или декремент не являются частью какого-либо выражения, а встречаются сами по себе, то очевидно, различия между ними не влияют на результат. То есть эффект от ++i и i++ в третьем выражении for-а одинаков.

Пример реализации структуры данных “стек”:

int stack[100] = 0; // Стек из максимум 100 значений
int len = 0;  // Число значений в стеке, изначально он пуст.

// Добавлять значения в стек мы можем следующим образом:
stack[len++] = new_value;  // Добавляем новый элемент в стек,
            // затем увеличиваем число элементов на 1.

// Извлечение значения из стека:
int extracted = stack[--len];
// Например, для массива из 10 элементов последний имеет
// индекс 9. Поэтому мы сначала уменьшаем len на 1 до 9,
// и извлекаем элемент с индексом 9.

Механизм вызова функций

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

void f(void)
{
    double x = sin(1);
    printf("Сунус одного радиана %lf\n", x);
}

Как будет выглядеть выделение и освобождение памяти под функции f, sin, printf, показано на следующем рисунке.

_images/stack.png

Указатели

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

Пример простой программы:

int main(void)
{
    int a = 10, b = 20;

    int *p;
    // Тип переменной p -- это (int *), то есть
    // "указатель на int".
    // Указатель p не инициализирован и указывает
    // на случайную ячейку памяти.

    p = &a;
    // присваиваем указателю адрес переменной a.

    *p = 123;
    // присваиваем значение ячейке памяти, на
    // которую указывает p.

    printf("address = %p, value = %d\n", p, *p);
    // p -- адрес указателя, *p -- значение, которое
    // хранится по этому адресу. %p -- это формат вывода
    // для указателей.

    printf("a = %d\n", a);
    // Выведет "a = 123", то есть мы действительно
    // изменили а через доступ по указателю p.

    // Ситуация на данный момент показана на рис. 1.

    int c;
    c = 1 + *p; // Чтение из ячейки памяти, на которую
                // указывает p.
                // Теперь c == 124

    p = &b; // Теперь p указывает на b.
    // Ситуация на данный момент показана на рис. 2.
    *p = 456;
    b = 42;
    // Ситуация на данный момент показана на рис. 3.
    // В предыдущих двух строках кода запись шла в одну
    // и ту же ячейку памяти.
    printf("a = %d, b = %d, *p = %d\n", a, b, *p);
    // Выведет: a = 123, b = 42, *p = 42

    return 0;
}
_images/pointers1.png

& – оператор получения адреса. Возвращает адрес переменной или ячейки массива.

Примеры: &a (адрес переменной), &arr[i] (адрес ячейки массива).

&(1 + 2) – ошибка. У результата (1 + 2) нет постоянного места в памяти.

* - оператор взятия значения по указателю. Возвращает значение ячейки памяти, на которую указывает указатель. Если тип указателя p – это double *, то тип *p – double.

Если p – это неинициализированный указатель, то *p в большинстве случаев приведёт к попытке доступа к недоступной нашей программе памяти, а *p = value в тех редких случаях, когда ошибки памяти не будет, приведёт к порче памяти. Поэтому указатели надо стараться правильно инициализировать так можно раньше:

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

или же вообще внести инициализацию в определение переменной:

int x;
int *p = &x;
// Здесь * не является оператором, а относится  к типу (int *).
// Эквивалентно int *p; p = &x;

Пример функции, которая меняет местами два числа

// Данная функция работать не будет, так как при её вызове
// x и y будут копиями тех значений, с которыми она была
// вызвана, и перестановка копий не повлияет на переменные
// другой функции.
void wrong_swap(int x, int y)
{
    int t = x;
    x = y;
    y = t;
}

// Для того, чтобы поменять местами два числа вне
// функции, обязательно нужно использовать указатели.
void swap(int *xp, int *yp)
{
    int t = *xp;
    *xp = *yp;
    *yp = t;
}

int main(void)
{
    int a = 10, b = 20;
    printf("Initially:        a = %d, b = %d\n", a, b);
    wrong_swap(a, b);
    printf("after wrong_swap: a = %d, b = %d\n", a, b);
    swap(&a, &b); // Вызываем функцию, передавая ей адреса a и b.
    printf("after swap:       a = %d, b = %d\n", a, b);
    return 0;
}

Иллюстрация работы обеих функций:

_images/swap.png