5. Глава 5: Указатели

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

5.1. Что такое указатель?

Указатель — это переменная, которая хранит адрес памяти другой переменной. Это не само значение, а «ссылка» на место, где это значение хранится. Думайте об указателе как о почтовом адресе дома, а не о самом доме.

Два ключевых оператора для работы с указателями:

  • & (оператор «адрес»): Возвращает адрес памяти переменной.

  • * (оператор «разыменование»): Возвращает значение, хранящееся по адресу, на который указывает указатель.

Пример (`pointer_intro.c`):

int a = 10;      // Объявляем целочисленную переменную 'a' и присваиваем ей значение 10.
int *p = &a;     // Объявляем указатель 'p' на тип int и инициализируем его адресом переменной 'a'.

printf("Значение a: %d\n", a);         // Выводит значение переменной 'a' (10).
printf("Адрес a: %p\n", &a);           // Выводит адрес памяти переменной 'a' (например, 0x7ffee45b7abc).
printf("Значение указателя p (адрес, который он хранит): %p\n", p); // Выводит тот же адрес, что и '&a'.
printf("Значение, на которое указывает p (*p): %d\n", *p); // Выводит значение, хранящееся по адресу, на который указывает 'p' (т.е. значение 'a', которое равно 10).

5.2. Как объявить указатель?

Для объявления указателя используется синтаксис: тип *имя_указателя;

Где тип — это тип данных переменной, на которую будет указывать указатель. Звездочка * указывает компилятору, что переменная имя_указателя является указателем.

Пример (`declare_pointer.c`):

float *ptr;      // Объявляем указатель 'ptr', который может хранить адрес переменной типа float.

// Указатель можно инициализировать адресом совместимого типа переменной:
float x = 3.14;  // Объявляем переменную 'x' типа float.
ptr = &x;        // Присваиваем указателю 'ptr' адрес переменной 'x'.

printf("Значение x: %f\n", x);
printf("Адрес x: %p\n", &x);
printf("Значение ptr (адрес x): %p\n", ptr);
printf("Значение, на которое указывает ptr (*ptr): %f\n", *ptr);

5.3. Работа с указателями (изменение значения)

Используя оператор разыменования *, мы можем не только читать значение, на которое указывает указатель, но и изменять его. Это изменяет оригинальную переменную, на которую указывает указатель.

Пример (`modify_via_pointer.c`):

int x = 5;       // Объявляем целочисленную переменную 'x' со значением 5.
int *p = &x;     // Объявляем указатель 'p' и инициализируем его адресом 'x'.

printf("До изменения: x = %d\n", x); // Выведет 5

*p = 10;         // Используем оператор разыменования '*' для доступа к значению по адресу,
                 // на который указывает 'p', и присваиваем ему новое значение 10.
                 // Это фактически изменяет значение переменной 'x' на 10.

printf("После изменения: x = %d\n", x); // Теперь 'x' будет равно 10
printf("Значение через *p: %d\n", *p);  // И, конечно, *p также будет равно 10

5.4. Передача по указателю (Call by Reference)

В языке C функции передают аргументы по значению (call by value). Это означает, что внутри функции создаётся копия переданной переменной, и любые изменения этой копии не влияют на оригинал.

Однако, если мы передадим адрес переменной (то есть указатель) в функцию, функция сможет получить доступ к оригинальной переменной и изменить её. Этот механизм называется передачей по ссылке (call by reference) через указатель.

Пример (`call_by_reference.c`):

// Функция принимает указатель на int.
// 'n' здесь - это локальная переменная-указатель, которая будет хранить адрес.
void setZero(int *n) {
    // Разыменовываем указатель 'n' и присваиваем 0 значению,
    // которое находится по адресу, хранимому в 'n'.
    // Это изменяет оригинальную переменную, переданную по адресу.
    *n = 0;
}

int main() {
    int a = 42; // Объявляем переменную 'a' со значением 42.
    printf("До вызова функции: a = %d\n", a); // Выведет 42

    // Вызываем функцию setZero, передавая ей АДРЕС переменной 'a' (&a).
    setZero(&a);

    printf("После вызова функции: a = %d\n", a); // Теперь 'a' будет равно 0
    return 0;
}

5.5. Указатель на указатель

В C можно создавать указатели, которые указывают на другие указатели. Это полезно, например, при работе с многомерными массивами или при необходимости изменить сам указатель внутри функции.

Синтаксис: тип **имя_указателя_на_указатель; (две звездочки).

Пример (`pointer_to_pointer.c`):

int x = 7;       // Переменная 'x' со значением 7.
int *p = &x;     // Указатель 'p' хранит адрес 'x'. 'p' указывает на 'x'.
int **pp = &p;   // Указатель 'pp' хранит адрес 'p'. 'pp' указывает на 'p', который указывает на 'x'.

printf("Значение x: %d\n", x);           // 7
printf("Адрес x: %p\n", &x);             // Адрес 'x'
printf("Значение p (адрес x): %p\n", p); // Адрес 'x'
printf("Адрес p: %p\n", &p);             // Адрес 'p'
printf("Значение pp (адрес p): %p\n", pp); // Адрес 'p'

// Разыменовываем pp один раз (*pp) - получаем значение p (т.е. адрес x).
// Разыменовываем pp дважды (**pp) - получаем значение по адресу, на который указывает p (т.е. значение x).
printf("Значение через *p: %d\n", *p);     // 7 (Значение x)
printf("Значение через **pp: %d\n", **pp); // 7 (Значение x, полученное через p, которое получено через pp)

Указатели открывают прямой доступ к памяти, позволяют писать более эффективные функции, особенно для работы с большими структурами данных, и являются неотъемлемой частью работы с массивами, строками и динамической памятью. Понимание их принципов — и вы получите мощный инструмент в арсенал программиста на C!