NULL pointer - нулевой указатель




У казатель до инициализации хранит мусор, как и любая другая переменная. Но в то же время, этот "мусор" вполне может оказаться валидным адресом. Пусть, к примеру, у нас есть указатель. Каким образом узнать, инициализирован он или нет? В общем случае никак. Для решения этой проблемы был введён макрос 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, то мы освобождаем память, то есть говорим компьютеру, что эта память может быть использована кем-то другим. Он может использовать нашу память, а может и нет, но теперь у нас уже нет гарантии, что эта память наша. При этом сама переменная не зануляется, она продолжает хранить адрес, которым ранее пользовалась.

Это очень похоже на съём номера в отеле. Мы получаем дубликат ключа от номера, живём в нём, а потом сдаём комнату обратно. Но дубликат ключа у нас остаётся. Всегда можно зайти в этот номер, но в нём уже кто-то может жить. Так что наша обязанность – удалить дубликат.

Иногда думают, что происходит "создание" или "удаление" памяти. На самом деле происходит только перераспределение ресурсов.



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2016-02-16 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: