Тип void на языке C Печать
Добавил(а) microsin   

Смысл типа void и его предназначение тесно связаны с основной, самой мощной (и при неумелом применении деструктивной) особенностью языка C - использование указателей. Поэтому сначала нужно разобраться, что такое указатели и для чего они нужны (кто не знает, что такое указатели, см. врезку ниже).

Для работы со встраиваемыми системами критически важно хорошо программировать на языке C и иметь четкое представление о смысле указателей (здесь приведен перевод статьи [1]). Указатель так важен потому, что он позволяет программисту получить доступ к памяти системы самым быстрым и эффективным способом (что для встраиваемых систем критически важно). Память системы организована как последовательность байт (1 байт состоит из 8 битов). Если, к примеру, общая память в системе имеет размер 128 байт, то здесь будет 128 доступных ячеек, каждая из которых будет байтом, который можно прочитать и записать. Все 128 ячеек памяти будут пронумерованы числами от 0 до 127 специальным способом, примерно так: 0000, 0001, 0002, ... и т. д. Это число, связанное с каждым байтом, называется адресом ячейки памяти.

Ниже на рисунке для иллюстрации всей идеи показана организация памяти некогда очень популярной архитектуры 8051.

MCS 51 Program Memory Arrangement examples

Указатель это переменная, которая содержит адрес ячейки памяти. Если, к примеру, адрес ячейки 2050H, то указатель используется для того, чтобы хранить в себе это значение адреса.

Примечание: адрес ячейки памяти это всегда положительное целое число. Диапазон адресов простирается от 0 (адрес первой ячейки памяти; часто этот адрес имеет специальное назначение, об этом позже) до положительной целочисленной константы (которая является адресом последней ячейки памяти).

[Переменные указателей]

Мы можем использовать переменные для хранения адресов памяти, и такие переменные известны как переменные указателей. Обычные переменные используются для хранения в себе значений каких-то данных определенного типа  (char, int, float и т. д.). Перед использованием переменной в программе мы сначала её декларируем. Специальным образом нам нужно также декларировать переменные и для указателей – чтобы компилятор знал, что мы декларируем переменную как указатель (это не обычная переменная). Делается такая декларация с помощью оператора *, это так называемый оператор косвенного обращения на языке C (indirection operator), иногда его называют оператором разыменования.

Общий синтаксис декларации указателя следующий:

тип_данных *имя_переменной;

Пример:

int *ptr;

Здесь мы декларировали переменную указателя с именем ptr, и этот указатель предназначен для указания на первую ячейку памяти, где находится значение типа int.

Почему для указателей нужно указывать тип данных? По некоторому адресу в памяти могут содержаться какие-то данные, и это понятно. И это может быть данные любого типа char, int, float, даже структура, и т. д. Разница между типами в том, что они могут занимать для себя разное количество памяти. Char требует 1 байт, int может требовать 2 байта (хотя это не всегда так), и float занимает 4 байта. Память, выделенная для всех этих типов это последовательность из нескольких непрерывно следующих друг за другом байт.

Давайте рассмотрим сценарий наподобие следующего, в программе определены 3 переменные:

char a;
int b;
float c;

Предположим, что память системы начинается с адреса 2000H. Теперь символьная переменная 'a' будет находиться по адресу 2000H (и займет в памяти 1 байт), тогда как int-переменная 'b' займет 2 байта и будет находиться по адресам 2001H и 2002H. И наконец, последняя float-переменная 'c' займет 4 байта, и они будут находится в расположенных друг за другом байтах памяти с адресами 2003H, 2004H, 2005H, 2006H. Теперь Вы можете догадаться, зачем надо указывать типы данных, чтобы объявить переменную указателя. Причина в том, что области памяти под переменные выделяются в последовательных, находящихся друг за другом байтах памяти, и количество выделенных байт зависит от типа переменной, которая в них содержится.

Примечание: показанное в этом примере распределение памяти типично для 8-разрядных систем, таких как MSC-51 и AVR. Для 16-битных и 32-разрядных систем реальное распределение памяти для переменных может быть другим, что связано с выравниванием данных в памяти с целью более эффективного использования особенностей процессора.

Таким образом, когда мы декларируем переменную указателя как float *ptr, и затем присваиваем ей адрес обычной float-переменной c, то для компилятора устанавливается привязка указателя ptr ко всей области памяти от 2003H до 2006H. Но сама переменная ptr будет хранить в себе только начальный адрес блока памяти переменной (т. е. в нашем примере это 2003H), а тип указателя будет указывать для компилятора размер этого блока.

Следовательно, чтобы компилятор мог корректно интерпретировать содержимое памяти, соответствующее указателю, для указателя должен быть при декларации указан тип данных. И этот тип данных должен совпадать с типом данных, которые находятся по адресу переменной – тому адресу, который присваивается переменной указателя. Например, если адрес 2000H будет присвоен указателю ptr, то указателю будет соответствовать память, в которой находится символьная переменная 'a'. В этом случае переменная указателя ptr должна быть декларирована с типом char, как показано ниже:

char *ptr;

Примечание: фактически мы могли бы декларировать переменную указателя без какого либо типа данных, используя ключевое слово void. Тогда получится так называемый пустой указатель.

[Присваивание адреса переменной указателя]

Чтобы можно было использовать указатель и его возможности, указателю должен быть присвоен адрес переменной. Указателю можно присвоить адрес как одиночной переменной, так и адрес массива, так и адрес структуры, и адрес переменной класса, и даже адрес переменной указателя. Это делает указатели особенно мощным (но и достаточно опасным при неумелом использовании) инструментом в программировании на языке C. Мы можем играючи обращаться с памятью системы, используя указатели.

Чтобы присвоить адрес переменной указателя мы используем оператор & (оператор взятия адреса переменной). Оператор & возвратит начало места в памяти, где расположена переменная. Пример:

void main()
{
   int *ptr;   // Декларация переменной указателя типа int.
   int a;      // Декларация обычной переменной типа int. 
   ptr = &a;   // Присваивание адреса переменной a указателю ptr.
}

[Обращение к содержимому памяти по указателю]

Теперь мы знаем, как присваивать адрес переменной указателя. Но как можно в программе обратиться к содержимому переменной, адрес которой присвоен указателю? Для этого мы используем тот же оператор косвенного обращения *, который мы использовали для декларации переменной указателя. Операция взятия значения по указателю также называется разыменованием указателя.

Рассмотрим сценарий:

void main()
{
   int *ptr;   // Декларация переменной указателя ptr на тип int.
   int a = 10; // Декларация обычной переменной a, и присваивание ей значения 10.
   ptr = &a;   // Присваивание адреса переменной a указателю ptr.
   // Следующий оператор позволит увидеть значение переменной a, полученное
   // с помощью указателя ptr:
   printf("Значение, на которое показывает указатель = %d", *ptr);
}

Запуск этого кода выведет следующую строку:

Значение, на которое показывает указатель = 10

[Арифметические операции над указателями]

С типизованным указателями указателями (т. е. с такими указателями, для которых для декларации явно указан тип) можно использовать многие операции, которые доступны с целыми числами без знака: декремент --, инкремент ++, прибавление и вычитание константы. При этом будет меняться адрес, сохраненный в указателе на величину, кратную размеру типа указателя. Например, если прибавить к указателю на float константу 1, то сохраненный в указателе адрес увеличится на 4, потому что размер типа float равен 4 байтам.

void main()
{
   float *ptr;
   ptr = (float*)(0x2000);
   //Выведется значение 0x2000:
   printf("ptr = 0x%04X\r\n", ptr);
   ptr = ptr+1;
   //Выведется значение 0x2004:
   printf("ptr = 0x%04X\r\n", ptr);
}

[Указатели void на языке C]

Обычно переменная указателя декларируется с указанием типа данных содержимого, которое хранится в том месте памяти, на которое ссылается указатель (перевод статьи [2]). Примеры:

char  *ptr1;
int   *ptr2;
float *ptr3;

Переменная указателя, декларированная на определенный тип, не может содержать в себе адрес переменной другого типа. Это неправильно, и приведет к сообщению об ошибке при компиляции. Пример:

char *ptr;
int  var1;
 
ptr = &var1;   // Это неправильно, потому что ptr должен указывать на переменную типа char.

На языке C есть возможность создать указатель на неопределенный тип, так называемый "пустой указатель" (void pointer). Указатель на void это просто переменная указателя, которая декларирована с зарезервированным на языке C ключевым словом void. Пример:

void *ptr;     // Теперь ptr это переменная указателя общего назначения

Когда указатель декларируется с ключевым словом void, он становится универсальным. Это значит, что ему может быть присвоен адрес переменной любого типа (char, int, float и т. д.), и это не будет ошибкой.

[Разыменование void-указателя]

Как делается разыменование типизованных указателей с помощью оператора *, Вы уже знаете (если нет, то см. врезку "Что такое указатель"). Но в случае указателя на void нужно использовать приведение типа (typecast) переменной указателя, чтобы выполнить её разыменование (выполнить обращение к содержимому памяти, на которую ссылается void-указатель). Причина в том, что с void-указателем не связан никакой тип, и для компилятора нет никакого способа автоматически узнать, как обращаться к содержимому памяти, связанному с void-указателем. Таким образом, чтобы получить данные, на который ссылается void-указатель, мы делаем приведение указателя к корректному типу данных, которые находятся по адресу, содержащемуся в void-указателе.

Пример программы:

void main()
{
   int a = 10;
   float b = 35.75;
   void *ptr;     // Декларация void-указателя.
 
   ptr = &a;      // Присвоение адреса целочисленной переменной void-указателю.
 
   //Ниже приведен пример взятия значения целочисленной переменной
   //с помощью void-указателя. Здесь (int*)ptr используется для
   //приведения типа, и *((int*)ptr) будет ссылаться на содержимое
   //памяти, где находится переменная a.
   printf("Значение целочисленной переменой = %d", *((int*)ptr));
 
   ptr=&b;        // Присвоение адреса переменной типа float void-указателю.
 
   //Ниже приведен пример взятия значения float-переменной с помощью void-указателя.
   printf("Значение переменной типа float = %f", *((float*)ptr));
}

Указатели void полезны для программиста, когда заранее неизвестно о типе данных, которые поступают на вход программы. Типичным примером могут служить библиотечные функции манипулирования блоками памяти memcpy, memset и т. п. С помощью void-указателя программист может сослаться на место размещения данных произвольного, заранее неизвестного типа данных. Программа, к примеру, может быть написана таким образом, чтобы запросить у пользователя, какое приведение типа нужно использовать, чтобы правильно обработать входные данные. Ниже приведен в качестве примера кусок подобного кода.

void funct (void *a, int z)
{
   //Значение переменной z будет показывать, на какой тип данных
   //ссылается параметр a, что будет использоваться для корректного
   //приведения типа.
   if(z==1)
   {
      //Пользователь ввел 1, что означает целый тип данных.
      printf("%d", *(int*)a);    //приведение к типу int
   }
   else if(z==2)
   {
      //2 соответствует символьному типу данных.
      printf("%c", *(char*)a);   //приведение к типу char
   }
   else if(z==3)
   {
      //3 соответствует данным с плавающей точкой.
      printf("%f", *(float*)a);  //приведение к типу float
   }
}

При использовании void-указателей следует помнить, что для них недопустимы арифметические операции, как для типизованных указателей (см. врезку "Что такое указатель"). Пример:

void *ptr;
int a;
 
ptr = &a;
//Следующий оператор инкремента недопустим и приведет к ошибке при компиляции,
//потому что компилятор не получил никакой информации о том, на какую величину
//нужно изменить адрес, хранящийся в void-указателе.
ptr++;

[Ссылки]

1. Introduction to pointers in C site:circuitstoday.com.
2. Void pointers in C site:circuitstoday.com.