Глава 5: Указатели ================== Указатели позволяют напрямую работать с адресами памяти. Это основа эффективной и низкоуровневой работы в языке C. Понимание указателей критически важно для эффективного использования языка C, так как они лежат в основе многих его мощных возможностей, включая работу с массивами, динамическим выделением памяти и передачу данных в функции. .. contents:: :local: :depth: 2 Что такое указатель? -------------------- **Указатель** — это переменная, которая хранит **адрес памяти другой переменной**. Это не само значение, а "ссылка" на место, где это значение хранится. Думайте об указателе как о почтовом адресе дома, а не о самом доме. **Два ключевых оператора для работы с указателями:** * ``&`` (оператор "адрес"): Возвращает **адрес памяти** переменной. * ``*`` (оператор "разыменование"): Возвращает **значение**, хранящееся по адресу, на который указывает указатель. **Пример (`pointer_intro.c`):** .. code-block:: 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). Как объявить указатель? ------------------------ Для объявления указателя используется синтаксис: ``тип *имя_указателя;`` Где ``тип`` — это тип данных переменной, на которую будет указывать указатель. Звездочка ``*`` указывает компилятору, что переменная ``имя_указателя`` является указателем. **Пример (`declare_pointer.c`):** .. code-block:: 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); Работа с указателями (изменение значения) ----------------------------------------- Используя оператор разыменования ``*``, мы можем не только читать значение, на которое указывает указатель, но и **изменять его**. Это изменяет оригинальную переменную, на которую указывает указатель. **Пример (`modify_via_pointer.c`):** .. code-block:: 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 Передача по указателю (Call by Reference) ----------------------------------------- В языке C функции передают аргументы **по значению (call by value)**. Это означает, что внутри функции создаётся копия переданной переменной, и любые изменения этой копии не влияют на оригинал. Однако, если мы передадим **адрес переменной** (то есть указатель) в функцию, функция сможет получить доступ к оригинальной переменной и изменить её. Этот механизм называется **передачей по ссылке (call by reference)** через указатель. **Пример (`call_by_reference.c`):** .. code-block:: 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; } Указатель на указатель ---------------------- В C можно создавать указатели, которые указывают на другие указатели. Это полезно, например, при работе с многомерными массивами или при необходимости изменить сам указатель внутри функции. **Синтаксис:** ``тип **имя_указателя_на_указатель;`` (две звездочки). **Пример (`pointer_to_pointer.c`):** .. code-block:: 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!