Правила программирования на С и С++. Главы 1-6
Страница 77. Опасайтесь приведения типов (спорные вопросы С)


70. Опасайтесь приведения типов (спорные вопросы С).

Оператор приведения типов часто понимается неправильно. Приведение типов не указывает компилятору "считать эту переменную принадлежащей к этому типу". Оно должно рассматриваться как операция времени выполнения, которая создает временную переменную типа, определенного для приведения, затем инициализирует эту временную переменную от операнда. В С++, конечно, эта инициализация может обернуться очень большими накладными расходами, так как возможен вызов конструктора.

Первое место, где неверное понимание приведения типов может навлечь на вас неприятности, находится в С, где не требуются прототипы функций. Когда компилятор находит вызов функции без предшествующего прототипа, то полагает, что эта функция возвращает тип int. В следующем фрагменте не говориться "malloc() на самом деле возвращает указатель, а не тип int":

int *p = (int *) malloc( sizeof(int) );а скорее код говорит "я полагаю, что malloc() возвращает тип int, так как тут нет предшествующего прототипа, и преобразую этот int в указатель для присваивания его значения p). Если тип int имеет размер 16 бит, а указатель 32-битовый, то вы теперь в глубокой луже. Вызов malloc() может вернуть и 32-битовый указатель, но так как компилятор полагает, что malloc() возвращает 16-битовый int, то он игнорирует остальные 16 бит. Затем компилятор округляет возвращенное значение до 16-бит и преобразует его в 32-битовый тип int принятым у него способом, обычно заполняя старшие 16 бит нулями. Если указатель содержал адрес больше, чем 0xffff, что вероятно для большинства компьютеров, то вы просто теряете старшие биты. Единственным способом урегулирования этой проблемы является указание для malloc() соответствующего прототипа, который подскажет, что malloc() возвращает указатель (обычно путем включения файла ?stdlib.h>).

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

f( int x );

// ...

unsigned y;

f( y );

и многие программисты заглушат такой компилятор при помощи f((int)y). Несмотря на это, приведение типа не изменит того факта, что тип unsigned int может содержать такое значение, которое не поместится в int со знаком, поэтому результирующий вызов может не сработать.

Вот сходная проблема, связанная с указателями на функции. Следующий код, случается, работает отлично:

some_object array[ size ];

int my_cmp( some_object *p1, some_object *p2 );

qsort( array, size, sizeof(some_object), ((*)(void*, void*)) my_cmp );

Следующий похожий код просто печально отказывается работать без предупреждающего сообщения: some_object array[ size ];

void foo( int x );

qsort( array, size, sizeof(some_object), ((*)(void*, void*)) foo );

Функция qsort() передает аргументы-указатели в foo(), но foo() ждет в качестве аргумента int, поэтому будет использовать значение указателя в качестве int. Дальше еще хуже - foo() вернет мусор, который будет использован qsort(), так как она ожидает в качестве возвращаемого значения int.

Выравнивание также связано с затруднениями. Многие компьютеры требуют, чтобы объекты определенных типов располагались по особым адресам. Например, несмотря на то, что 1-байтоый тип char может располагаться в памяти по любому адресу, 2-байтовый short должен будет иметь четный адрес, а 4-байтовый long - четный и кратный четырем. Следующий код вновь не выдаст предупреждений, но может вызвать зависание компьютера во время выполнения:

short x;

long *lp = (long*)( ?x );

*lp = 0;

Эта ошибка особенно опасна, потому что *lp = 0 не сработает лишь тогда, когда x окажется по нечетному или не кратному четырем адресу. Может оказаться, что этот код будет работать до тех пор, пока вы не добавите объявление второй переменной типа short сразу перед x, после чего эта программа зависнет.

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

p = (char *)(long *);

 
« Предыдущая статья