Правила программирования на Си и Си++

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


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

Первое место, где неверное понимание приведения типов может навлечь на вас неприятности, находится в Си, где не требуются прототипы функций. Когда компилятор находит вызов функции без предшествующего прототипа, то полагает, что эта функция возвращает тип 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 *);

Содержание раздела