Тип void на языке C |
![]() |
Добавил(а) microsin |
Смысл типа void и его предназначение тесно связаны с основной, самой мощной (и при неумелом применении деструктивной) особенностью языка C - использование указателей. Поэтому сначала нужно разобраться, что такое указатели и для чего они нужны (кто не знает, что такое указатели, см. врезку ниже). Для работы со встраиваемыми системами критически важно хорошо программировать на языке C и иметь четкое представление о смысле указателей (здесь приведен перевод статьи [1]). Указатель так важен потому, что он позволяет программисту получить доступ к памяти системы самым быстрым и эффективным способом (что для встраиваемых систем критически важно). Память системы организована как последовательность байт (1 байт состоит из 8 битов). Если, к примеру, общая память в системе имеет размер 128 байт, то здесь будет 128 доступных ячеек, каждая из которых будет байтом, который можно прочитать и записать. Все 128 ячеек памяти будут пронумерованы числами от 0 до 127 специальным способом, примерно так: 0000, 0001, 0002, ... и т. д. Это число, связанное с каждым байтом, называется адресом ячейки памяти. Ниже на рисунке для иллюстрации всей идеи показана организация памяти некогда очень популярной архитектуры 8051. Указатель это переменная, которая содержит адрес ячейки памяти. Если, к примеру, адрес ячейки 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. |