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!