У казатель до инициализации хранит мусор, как и любая другая переменная. Но в то же время, этот "мусор" вполне может оказаться валидным адресом. Пусть, к примеру, у нас есть указатель. Каким образом узнать, инициализирован он или нет? В общем случае никак. Для решения этой проблемы был введён макрос NULL библиотеки stdlib.
Принято при определении указателя, если он не инициализируется конкретным значением, делать его равным NULL.
?
int *ptr = NULL; |
По стандарту гарантировано, что в этом случае указатель равен NULL, и равен нулю, и может быть использован как булево значение false. Хотя в зависимости от реализации NULL может и не быть равным 0 (в смысле, не равен нулю в побитовом представлении, как например, int или float).
Это значит, что в данном случае
?
int *ptr = NULL; if (ptr == 0) { ... } |
вполне корректная операция, а в случае
?
int a = 0; if (a == NULL) { ... } |
поведение не определено. То есть указатель можно сравнивать с нулём, или с NULL, но нельзя NULL сравнивать с переменной целого типа или типа с плавающей точкой.
?
#include <stdlib.h> #include <stdio.h> #include <conio.h> void main() { int *a = NULL; unsigned length, i; printf("Enter length of array: "); scanf("%d", &length); if (length > 0) { //При выделении памяти возвращается указатель. //Если память не была выделена, то возвращается NULL if ((a = (int*) malloc(length * sizeof(int)))!= NULL) { for (i = 0; i < length; i++) { a[i] = i * i; } } else { printf("Error: can't allocate memory"); } } //Если переменая была инициализирована, то очищаем её if (a!= NULL) { free(a); } getch(); } |
Примеры
Теперь несколько примеров работы с указателями
1. Пройдём по массиву и найдём все чётные элементы.
?
#include <conio.h> #include <stdio.h> void main() { int A[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int even[10]; int evenCounter = 0; int *iter, *end; //iter хранит адрес первого элемента массива //end хранит адрес следующего за последним "элемента" массива for (iter = A, end = &A[10]; iter < end; iter++) { if (*iter % 2 == 0) { even[evenCounter++] = *iter; } } //Выводим задом наперёд чётные числа for (--evenCounter; evenCounter >= 0; evenCounter--) { printf("%d ", even[evenCounter]); } getch(); } |
2. Когда мы сортируем элементы часто приходится их перемещать. Если объект занимает много места, то операция обмена местами двух элементов будет дорогостоящей. Вместо этого можно создать массив указателей на исходные элементы и отсортировать его. Так как размер указателей меньше, чем размер элементов целевого массива, то и сортировка будет происходить быстрее. Кроме того, массив не будет изменён, часто это важно.
?
#include <conio.h> #include <stdio.h> #define SIZE 10 void main() { double unsorted[SIZE] = {1.0, 3.0, 2.0, 4.0, 5.0, 6.0, 8.0, 7.0, 9.0, 0.0}; double *p[SIZE]; double *tmp; char flag = 1; unsigned i; printf("unsorted array\n"); for (i = 0; i < SIZE; i++) { printf("%.2f ", unsorted[i]); } printf("\n"); //Сохраняем в массив p адреса элементов for (i = 0; i < SIZE; i++) { p[i] = &unsorted[i]; } do { flag = 0; for (i = 1; i<SIZE; i++) { //Сравниваем СОДЕРЖИМОЕ if (*p[i] < *p[i-1]) { //обмениваем местами АДРЕСА tmp = p[i]; p[i] = p[i-1]; p[i-1] = tmp; flag = 1; } } } while(flag); printf("sorted array of pointers\n"); for (i = 0; i < SIZE; i++) { printf("%.2f ", *p[i]); } printf("\n"); printf("make sure that unsorted array wasn't modified\n"); for (i = 0; i < SIZE; i++) { printf("%.2f ", unsorted[i]); } getch(); } |
3. Более интересный пример. Так как размер типа char всегда равен 1 байт, то с его помощью можно реализовать операцию swap – обмена местами содержимого двух переменных.
?
#include <conio.h> #include <conio.h> #include <stdio.h> void main() { int length; char *p1, *p2; char tmp; float a = 5.0f; float b = 3.0f; printf("a = %.3f\n", a); printf("b = %.3f\n", b); p1 = (char*) &a; p2 = (char*) &b; //Узнаём сколько байт перемещать length = sizeof(float); while (length--) { //Обмениваем местами содержимое переменных побайтно tmp = *p1; *p1 = *p2; *p2 = tmp; //не забываем перемещаться вперёд p1++; p2++; } printf("a = %.3f\n", a); printf("b = %.3f\n", b); getch(); } |
В этом примере можно поменять тип переменных a и b на double или любой другой (с соответствующим изменением вывода и вызова sizeof), всё равно мы будет обменивать местами байты двух переменных.
4. Найдём длину строки, введённой пользователем, используя указатель
?
#include <conio.h> #include <stdio.h> void main() { char buffer[128]; char *p; unsigned length = 0; scanf("%127s", buffer); p = buffer; while (*p!= '\0') { p++; length++; } printf("length = %d", length); getch(); } |
Обратите внимание на участок кода
?
while (*p!= '\0') { p++; length++; } |
его можно переписать
?
while (*p!= 0) { p++; length++; } |
или
?
while (*p) { p++; length++; } |
или, убрав инкремент в условие
?
while (*p++) { length++; } |
Malloc
В предыдущей главе уже обсуждалось, что локальные переменные кладутся на стек и существую до тех пор, пока мы не вышли из функции. С одной стороны, это позволяет автоматически очищать память, с другой стороны, существует необходимость в переменных, время жизни которых мы можем контролировать самостоятельно. Кроме того, нам необходимо динамическое выделение памяти, когда размер используемого пространства заранее не известен. Для этого используется выделение памяти на куче. Недостатков у такого подхода два: во-первых, память необходимо вручную очищать, во-вторых, выдеение памяти – достаточно дорогостоящая операция.
Для выделения памяти на куче в си используется функция malloc (memory allocation) из библиотеки stdlib.h
?
void * malloc(size_t size); |
Функция выделяет size байтов памяти и возвращает указатель на неё. Если память выделить не удалось, то функция возвращает NULL. Так как malloc возвращает указатель типа void, то его необходимо явно приводить к нужному нам типу. Например, создадим указатель, после этого выделим память размером в 100 байт.
?
#include <conio.h> #include <stdio.h> #include <stdlib.h> void main() { int *p = NULL; p = (int*) malloc(100); free(p); } |
После того, как мы поработали с памятью, необходимо освободить память функцией free.
Используя указатель, можно работать с выделенной памятью как с массивом. Пример: пользователь вводит число – размер массива, создаём массив этого размера и заполняем его квадратами чисел по порядку. После этого выводим и удаляем массив.
?
#include <conio.h> #include <stdio.h> #include <stdlib.h> void main() { const int maxNumber = 100; int *p = NULL; unsigned i, size; do { printf("Enter number from 0 to %d: ", maxNumber); scanf("%d", &size); if (size < maxNumber) { break; } } while (1); p = (int*) malloc(size * sizeof(int)); for (i = 0; i < size; i++) { p[i] = i*i; } for (i = 0; i < size; i++) { printf("%d ", p[i]); } getch(); free(p); } |
Разбираем код
?
p = (int*) malloc(size * sizeof(int)); |
Здесь (int *) – приведение типов. Пишем такой же тип, как и у указателя.
size * sizeof(int) – сколько байт выделить. sizeof(int) – размер одного элемента массива.
После этого работаем с указателем точно также, как и с массивом. В конце не забываем удалять выделенную память.
Теперь представим на рисунке, что у нас происходило. Пусть мы ввели число 5.
Выделение памяти.
Функция malloc выделила память на куче по определённому адресу, после чего вернула его. Теперь указатель p хранит этот адрес и может им пользоваться для работы. В принципе, он может пользоваться и любым другим адресом.
Когда функция malloc "выделяет память", то она резервирует место на куче и возвращает адрес этого участка. У нас будет гарантия, что компьютер не отдаст нашу память кому-то ещё. Когда мы вызываем функцию free, то мы освобождаем память, то есть говорим компьютеру, что эта память может быть использована кем-то другим. Он может использовать нашу память, а может и нет, но теперь у нас уже нет гарантии, что эта память наша. При этом сама переменная не зануляется, она продолжает хранить адрес, которым ранее пользовалась.
Это очень похоже на съём номера в отеле. Мы получаем дубликат ключа от номера, живём в нём, а потом сдаём комнату обратно. Но дубликат ключа у нас остаётся. Всегда можно зайти в этот номер, но в нём уже кто-то может жить. Так что наша обязанность – удалить дубликат.
Иногда думают, что происходит "создание" или "удаление" памяти. На самом деле происходит только перераспределение ресурсов.