Увод
В настоящата статия ще говорим за void указателите в езика C. Забележете – C, а не C++, тъй като указателите оригинално произлизат от C, а поддръжката им в C++ е изцяло наследена от C, и C като цяло е по-добре стандартизиран и по-прост език от C++. Въпреки това, концепциите, техниките и примерите, които ще покажа са почти напълно приложими и в C++.
И така, ако вече сте на “ти” с използването на указатели, а термина “void указател” ви звучи странно защото сте се заблудили, че всеки указател трябва да има тип, то тогава е време да научите нещо ново за невероятната гъвкавост която предоставя единствения по рода си език – C.
Един по-дълбок поглед върху указателите
Сами по себе си, всички указатели, независимо дали са декларирани от тип char *, int *, FILE *, struct mystruct *, mytype_t *, или от който и да е друг вграден или дефиниран от потребителя тип, имат фиксиран размер и на машинно ниво не се различават с нищо по между си. Размерите им са фиксирани, това обикновено е една машинна дума. На практика, в езика C всеки указател е с дължината на int, а int е една машинна дума (с дължина съответно 2/4/8 байта при 16/32/64 битови системи). Мислете за указателите като за нормални числови променливи, със свойството, че числото което съдържат е всъщност адрес в паметта. Сега сигурно се питате: защо в такъв случай указателите изобщо трябва да се декларират с тип? Всъщност, понятието “тип на указател” е една абстракция която показва на компилатора колко байта е дължината на соченият блок от памет (а не на самия указател!), като се започне от указания начален адрес. Когато указателя се “дереференцира”, т.е. извлича се сочената стойност, това има значение защото компилатора трябва да знае точно колко е дължината на сочения блок. Ако ви е трудно да си представите горната картина, разгледайте следното:
Паметта всъщност представлява един дълъг масив от байтове, където всеки байт (елемент) има адрес (индекс) и стойността на този байт може да се извлече като се индексира чрез адреса. Както сигурно знаете, един байт съдържа 8 бита, а това прави възможно съществуването на 2^8 = 256 = 0x100 различни стойности вътре.
Декларирали сме два указателя, myptr1 и myptr2. Както виждате, независимо че единият сочи към тип short, а другият към mytype, и двата са с дължина 4 байта (зелено оцветените клетки, приемаме 32 битова система). Стойността на всеки един от тези указатели всъщност е число, което представлява начален адрес на блок от паметта, съответно 533 (0x00000215) и 536 (0x00000218). Както всички останали променливи, дори и самите указатели имат адрес в паметта където са разположени – съответно 548 и 543. Приемаме също, че short е с дължина 2 байта, а mytype е потребителски дефиниран тип с дължина 5. От илюстрацията сигурно забелязвате, че единствената операция, заради която указателите трябва да имат тип е операцията “извличане на сочената стойност” или по-краткото “дереференциране”. С други думи, компилатора трябва да покаже на програмата с колко байта нататък от началния адрес да се чете, за да се извлече правилната стойност на променливата. За short това е 2, за mytype е 5 (синьо оцветените клетки).
Какво е void указател?
Мислете за void указателите като за нормални указатели, с единствената разлика, че не могат да се дереференцират. Те се наричат още указатели от неопределен, общ тип, или “generic pointers”. Те съдържат адрес както всички останали указатели, с разликата, че не показват на компилатора колко байта е дължината на променливата след този адрес. С други думи, ако sizeof(short) е 2, sizeof(mytype) е 5, то sizeof(void) е неопределен.
Сега сигурно се питате, защо ми е указател, който не може да се дереференцира? Мощността и гъвкавостта на void указателите идва от факта, че те могат да се преобразуват във всеки един друг тип указател. Като пример, можете да преобразувате void * във short *, mytype *, и т.н. След като вече сте го преобразували, вие може да извлечете сочената стойност. Ето и един богат пример:
#include <stdio.h>
typedef struct {
short a;
char b[3];
} mytype;
int main (void)
{
int x = 5;
float y = 27.4825;
char z = 'K';
mytype myvar = {12345, "ab"};
void *vptr;
vptr = &x;
/* изкарва “x = 5” */
printf("x = %d\n", *((int *) vptr));
vptr = &y;
/* изкарва “y = 27.4825” */
printf("y = %f\n", *((float *) vptr));
vptr = &z;
/* изкарва “z = K” */
printf("z = %c\n", *((char *) vptr));
vptr = &myvar;
/* изкарва “myvar.a = 12345, myvar.b = ab” */
printf("myvar.a = %d, myvar.b = %s\n",
((mytype *) vptr)->a,
((mytype *) vptr)->b
);
return 0;
}
В горния код имаме четири променливи от различен тип, като последната е от потребителски дефинирания тип mytype. След това декларираме и нашия void указател. Чрез оператора “&” насочваме указателя към началния адрес на съответната променлива. Сега вече vptr сочи на точния начален адрес. Проблема е, че ако искаме да извлечем стойността, към която vptr сочи, не можем да го направим директно, защото компилатора не знае колко байта нататък от първия сочен байт се простира променливата. Затова преобразуваме vptr в съответния тип указател (int *, float *, char *, mytype *), и използваме оператора звезда (или съответно стрелка за структури) за да го дереференцираме. Хитро, нали?
Как се използват на практика?
Предния код илюстрира много добре какво са void указателите, но на практика е безсмислено да се ползват void указатели (а въобще и указатели) в подобни ситуации. Истинската сила на void указателите идва когато те се ползват като връщани стойности и параметри на функции. Чудили ли сте се например, как е възможно функцията malloc() да връща резултат, който може да се присвои на указател от всякакъв тип? Или как функциите fwrite()/fread() могат да записват/четат блокове от всякакви типове, със всякаква дължина? Тези “чудеса” са възможни именно благодарение на void указателите.
Нека разгледаме една функция със следния прототип:
int myfunc (void *buf, size_t length)
При функция с подобен прототип, C компилатора ще ви позволи да я извикате с указател от какъвто и да е тип, без нужда от преобразуване към void *. При извикване също се очаква да бъде посочена и дължината на блока в байтове, като се започне от началния адрес. Всъщност size_t е дефиниран в стандартния хедър stddef.h като unsigned int, но е широко приета конвенция когато става въпрос за дължина на блок да се ползва size_t. Знаейки началния адрес и дължината на блока, функцията може да го третира байт по байт, без въобще да се интересува от какъв тип е. Ето и как бихме написали функцията:
typedef unsigned char ubyte;
int myfunc (void *buf, size_t length)
{
ubyte *p = buf;
size_t i = 0;
while (i++ < length) {
printf(“Byte %d: 0x%02X\n”, i, *p);
p++;
}
return 0;
}
За да можем да обходим блока байт по байт, декларираме локален за функцията указател и го насочваме към същия адрес, към който сочи и buf, параметъра на функцията. Сега вече p е указател към ubyte, и компилатора знае, че когато той трябва да се дереференцира (операцията *p) или измества (операцията p++), това трябва да стане точно с един байт, тъй като sizeof(unsigned char) = sizeof(ubyte) = 1. По същата логика, указателя p можеше да бъде от тип float * или mytype *, вместо от ubyte *, и тогава функцията щеше да третира блока не като последователност от байтове, а съответно като последователност от числа с плаваща запетая или последователност от структури. Ето как бихме извикали нашата функция:
char mystring[] = "test";
mytype mystruct = {12345, "ab"};
short myarray[] = {255, 128, 64, 32};
printf("String:\n");
myfunc(mystring, strlen(mystring));
printf("---\n");
printf("Struct:\n");
myfunc(&mystruct, sizeof(mytype));
printf("---\n");
printf("Array:\n");
myfunc(myarray, 4 * sizeof(short));
printf("---\n");
А ето и какво се извежда:
String:
Byte 1: 0x74
Byte 2: 0x65
Byte 3: 0x73
Byte 4: 0x74
---
Struct:
Byte 1: 0x39
Byte 2: 0x30
Byte 3: 0x61
Byte 4: 0x62
Byte 5: 0x00
Byte 6: 0x24
---
Array:
Byte 1: 0xFF
Byte 2: 0x00
Byte 3: 0x80
Byte 4: 0x00
Byte 5: 0x40
Byte 6: 0x00
Byte 7: 0x20
Byte 8: 0x00
---
Този подход е изключително полезен когато искате да пишете гъвкави функции, които оперират с паметта независимо от типа, например хеш функции, входно/изходни функции и т.н.
Сега, след като надникнахте по-дълбоко в света на указателите, предполагам сте се убедили колко прост е езика C. Всяка променлива се свежда до линейна поредица от байтове и различните типове са просто една абстракция която показва дължината на тази поредица като се започне от началния адрес. Чрез използване на void указатели буквално се научихте как да “цепите” паметта на байтове :-)
Очаквайте втора серия или “как да цепим байтовете на битове” използвайки побитовите оператори в C. Засега от мен happy coding, но все пак не се забравяйте по цял ден, излезте малко навън, птички пеят, готино е, и се старайте да не мислите за void указатели поне докато пресичате улицата или шофирате :-)
Автор: Благовест Буюклиев, 11.08.2007