Нетипизированные указатели и sizeof

Мы уже заметили, что пустой указатель NULL принадлежит любому типу указателя. Аналогично и некоторые операции с указателями могут по существу не зависеть от указуемого типа. Например, операция выделения памяти, которая размещает непрерывный кусок памяти и выдаёт указатель на его начало. Сама процедура выделения не знает, сколько значений и какого типа будут размещены в отведённой памяти, поскольку её параметром является просто целое число, указывающее, сколько байт надо выделить. Поэтому возвращаемый указатель должен быть некоего универсального типа, или, иными словами, нетипизированным. Аналогичной является операция копирования одного отрезка памяти в другой, для чего требуется знать только указатели на начала отрезков и размер копируемого отрезка.  В языке C такие указатели описываются с помощью псевдо-типа void:

extern void * malloc(int c);

extern void * memcpy(void * dest, void * source, int count);

Для того, чтобы этот указатель можно было типизировать, применяется приведение типов, как например, в

float * A =(float *) malloc(10);

В обратную сторону явное приведение не требуется: все указатели в случае необходимости автоматически приводятся к типу void*.

Основным достоинством такого подхода к реализации универсальных операций над указателями является его гибкость: программист может, пользуясь средствами языка, описать свои операции. Однако за гибкость, как часто бывает, приходится платить потерей надёжности. Так, в последнем примере размер выделяемой памяти никак не связан с размером типа float, даже если предположить, что выделяется память не на один элемент, а на несколько. Но ни при трансляции, ни при выполнении программы проконтролировать это невозможно.

Таким образом, для обеспечения надёжности и переносимости программ требуется передать подобной универсальной процедуре знание о типе аргументов. Для этой цели в языке C используется псевдо-функция sizeof, которая возвращает размер типа аргумента. Следующая таблица приводит несколько примеров, демонстрирующих, в частности, отличие массивов от указателей и эффект выравнивания:

Описание Размер (sizeof)
char c; 1
char * p; 4
char s[] = "abc"; 4
char a3[] = { 'a','b','c' }; 3
struct T1 { char x; short y; char z;} S1; 6
struct T2 { int x; char y; } S2; 8

    Поскольку значение аргумента для этой операции неважно, то аргумент не вычисляется, и более того, в качестве аргумента можно использовать сам тип, т.е. вместо sizeof(p) можно написать и sizeof(char*). Ввиду того, что размер любого типа определён статически, вызов sizeof является константным выражением и заменяется на конкретное значение во время трансляции. Таким образом, более надёжным было бы пример с malloc записать как

float * A =(float *) malloc(sizeof(*A) * 10);

что гарантирует, что будет выделена память для 10 элементов размера типа *A, т.е. float.

Знание об природе аргумента может быть передано и с помощью функциональных параметров. Например, для для реализации алгоритма сортировки массива достаточно знать размер элементов, их количество и функцию, сравнивающую два элемента, опять же специфицированных как void *. В любом случае, язык C полагается лишь на хороший стиль и аккуратность программиста, оставляя дыру в контроле типов. Так, будет легально с помощью манипуляций с приведением указателей

float f = 1.0;

int x = * (int *) (&f);

"взглянуть" на представление вещественного числа как на представление целого. Переменная x в результате может получить в зависимости от реализации, например, значение 1065353216.

 


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: