Как не надо программировать на С++

Одноименное бессмертное творение Уэллина (рецензия здесь) заставило меня вспомнить груду моих собственных страшных и смешных ошибок, коих за мой опыт работы подобралось немалое количество. Подборку буду время от времени пополнять, дабы напоминать самой себе о мимолетности оперативной памяти, небезграничности дискового пространства, незримом и постоянном присутствии несовершенств человеческого фактора, и служить немым укором лени, заставляющей меня раз за разом опускать нудные размышления о работе базовых принципов Лейбница, Тьюринга и фон Неймана.

Числа и строки

Все знают, что с помощью стандартной функции atoi можно преобразовать строку в число. Но вот как не надо это делать:
char* arr = "123456789";
char tmp;
tmp = arr[i];
int i = atoi(&tmp);
В некоторых случаях i оказывается верно вычисленной, однако иногда в ней получается мусор! Разберемся как работает atoi. Она работает со строкой. Учебные процедуры преобразования строки в число пишутся так: преобразуем первый символ в число, умножаем на 10, прибавляем следующий символ, преобразованный в число, умножаем на 10 и прибавляем следующий и так далее пока все символы не закончатся. Скорее всего, atoi действует так же. Когда же она заканчивается? Вероятно, когда заканчивается строка - то есть когда мы дойдем до символа �. А где в нашей &tmp будет �?! Правильно - в неопределенном месте. И поэтому наше число вычислится неправильно.
Правильным кодом будет например такой:
char* arr = "123456789";
char tmp[2];
tmp[0] = arr[i];
tmp[1] = '�';
int i = atoi(&tmp);

Динамическое приведение типов

Есть класс-интерфейс и класс-реализация. Имея класс-интерфейс, хотим узнать, является ли он на самом деле реализацией.
class CParentClass
{
// ...
};

class CChildClass : public CParentClass
{
// ...
};

CParentClass* theClassP = new CChildClass;
CChildClass* theClassC = dynamic_cast(theClassC);
В последней строке производится динамическое приведение типов. В этой строке классическая очепятка в прямом смысле слова - мы приводим только что описанный класс к самому себе. Далее содержимое theClassC неопределено.

Временные переменные

Имеем строку (std::string), содержащую имя файла. В зависимости от расширения файла мы должны устроить ветвление в программе. Мы знаем, что у типа AnsiString, имеющего конструктор из const char*, в которую можно преобразовать нашу строку, есть метод GetExtension(), возвращающий const char*, в которой содержится расширение, и которую можно сравнивать с помощью strcmp с другими текстовыми строками. Всего лишь немного преобразований типов...
std::string theString = "MyFile.txt";

const char* theExtension = AnsiString(theString.c_str()).GetExtension().c_str();

if(!strcmp(theExtension, "bmp"))
// ...
else if (!strcmp(theExtension, "jpeg"))
// ...
else if (!strcmp(theExtension, "gif"))
// ...
else
MessageBox(NULL, "Unknown format", "Error", MB_OK);
В theExtension сохраняется ссылка на временный объект - объект существовал, когда существовал класс AnsiString, у которого мы сделали GetExtension(), у которого мы сделали c_str(). Во время выполнения этой строки программы мы
а) создали новую переменную типа AnsiString, вызвали ее конструктор;
б) вызвали ее метод GetExtension(), вернувший нам объект AnsiString, содержащий нужную строку;
в) вызвали c_str(). Метод работает так же как c_str() для std::string, т.е. возвращает указатель на нечто, хранящееся внутри класса;
г) Сохранили указатель из пункта в.
После того как эта строка выполнена, вызывается деструктор AnsiString, вместе с ним указатель, вернувшийся нам, становится недействителен.
Правильно было бы либо завести отдельную переменную AnsiString, либо использовать функцию этого класса для сравнения, вместо strcmp. Например:
std::string theString = "MyFile.txt";

AnsiString asString = theString.c_str(); // вызовется конструктор AnsiString

if(!asString.GetExtension().AnsiCompare("bmp"))
// ...
else if (!asString.GetExtension().AnsiCompare("jpeg"))
// ...
else if (!asString.GetExtension().AnsiCompare("gif"))
// ...
else
MessageBox(NULL, "Unknown format", "Error", MB_OK);

Scanf

Вот пример использования С-функции scanf:
double theDouble;
scanf("%f", &theDouble);
printf("%f", theDouble);
Каково будет наше удивление, когда распечатается не то что было введено! Почему? Потому что при %f означает, что мы собираемся вводить число float, с плавающей точкой одинарной точности. А вводим double, с плавающей точкой двойной точности. Происходит: scanf берет что ему передали (с его точки зрения, это void*), приводит ко float (от исходного double отрезали вторую половину) и вводим в то что получилось (в первую половину нашего double). Теперь печатаем: printf получает double, конвертирует его во float (при этом отрезается его половина) и выводит содержимое оставшегося. В случае если числа хранятся "нижние байты сперва, верхние байты потом" - то, что мы печатаем, всегда равно 0, ибо ввод осуществлялся в другую половину double-числа. Осознать в чем ошибка можно только досконально изучив архитектуру компьютера и тонкости работы стандартной библиотеки.
Правильно будет делать так:
double theDouble;
scanf("%lf", &theDouble);
printf("%lf", theDouble);

Printf

Только что предложили вот такую вариацию применения printf:
printf("%f,%f,%d,%d", 0, 0, 0, 0);
На экран выводились два нуля и два мусорных числа. Давайте разберемся почему.
Функция printf работает с переменным числом параметров. Тип параметров задается спецификаторами в строке - первом аргументе. Соответственно, при вызове вышеописанной функции компилятор запихивает в стек четыре нуля размером в int. А далее при разборе переменного числа параметров компилятор, глядя на переданные модификаторы, вытащил из стека сначала два числа размером в float, а потом два числа размером int. Если вам так не повезло, что на вашей машине float и int имеют разный размер, то мы вылезем за пределы стека и будем выводить естественно мусор. Особенно неприятно оказывается, если именно на вашей машине размеры float и int совпадают, а на машине заказчика - нет...
Правильный вариант будет такой:
printf("%f,%f,%d,%d", 0.0, 0.0, 0, 0);

Комментарии

Комментарий профессионального программиста

Это для дилетантов.

По поводу третьего примера: Scanf.

По поводу третьего примера.

> scanf берет что ему передали (с его точки зрения, это void*),
> приводит ко float (от исходного double отрезали вторую половину)
> и вводим в то что получилось (в первую половину нашего double).
> Теперь печатаем: printf получает double, конвертирует его во float
> (при этом отрезается его половина) и выводит содержимое оставшегося.

При выполнении функции scanf происходит ввод числа типа float (поскольку формат указан %f) по адресу, начиная с которого расположена переменная. В результате переменной будет содержать старые значения бит, а часть - введенные новые, соответсвующие float числу. Таким образом, никакого приведения типа не происходит, просто модифицируется половина переменной.
При печати напротив, переданное через параметры число типа double именно приводится к типу float. Но, это действие, вероятнее всего (в intel архитектуре - точно), не сводится к "отрезанию половины" - изменяется разрядность порядка и разрядность мантиссы, поэтому не очень важно, в каком именно порядке хранятся байты. Результат будет зависеть от всей совокупности правил кодирования вещественных чисел.

Кстати, все сказанное не зависит от архитектуры и реализации функций, а относится к спецификациям языка. То, что printf получает float и double числа как double, следует из правил передачи параметров функциям с переменным числом аргументов.

Не сочтите за критику, просто уточнение, l.

По поводу коментария Baal on July 16, 2006

double d = 1.0;
> printf("%f", (float)d).
> Оказывается, правильно делал :-)

printf всегда рассматривает вещественные числа как double, так что единственный результат действий "на всякий случай" - потеря точности. Ее не видно в примере, но если заказать печать большего числа значащих цифр, то она сразу проявляется:
double d=1./3.;
printf("(float)d=%.16f\n",(float)d);
printf(" d=%.16f\n",d);

(float)d=0.3333333432674408
d=0.3333333333333333
C уважением, l.

К вопросу об инструкции

Полностью согласен, что чтение инструкций (и вообще, поиск нетривиальной информации где бы то ни было), совершенно не прививается студентам.

Даже писал об этом здесь: http://cgm.graphicon.ru/content/view/119/57/

Замечание к третьему примеру

Цитата: "Осознать в чем ошибка можно только досконально изучив архитектуру компьютера и тонкости работы стандартной библиотеки."
Мой вариант: избежать подобных ошибок поможет вдумчивое чтение документации, в частности "format specification fields for scanf function" в MSDN.
Кстати, столь любимый Вами Уэллин тоже любит подобные ошибки, которые вроде и не ошибки даже, а так - опечатки досадные (кстати, многие из них даже и не возникнут при использовании редактора с подстветкой синтаксиса).
Однако, судя по количеству вопросов/работ со стороны студентов, в которых поднимается проблема, описанная в третьем примере, непонимание действительно имеет место быть.
Как бы еще привить широким массам понимание аксиомы Хана: "Если ничего не помогает, прочтите наконец инструкцию". Вот где действительно поле для размышления...
С уважением, Yuri

Я предпочитаю c#, если уж говорить о языках со сборкой мусора. Ну а вообще это от задачи зависит. Есть задачи, где C/C - единственный вариант. И пока их довольно много, особенно в области графики, где требуется оптимизация.

Почитал про мучения с GetExtension и ужаcнулся. Все-таки microsoft со своим собственным набором классов ужасно испоритла красивую с оригинале концепцию С . Вот я на досуге читаю последнее издание Страуструпа - и радуюсь, настолько там все логично и красиво. А когда приходится запускать MSVC и пытаться использовать что-то из microsoft-specific - хочется стереть всю эту visual studio с диска...

P.S. Все-таки, насколько удобны языки с автоматической сборкой мусора... Пишите на perl, господа! ;)

Забавно, буквально в пятницу делал почти тоже самое, выводил информацию в консоль. Только (исключительно на всякий случай!) делал приведение.
double d = 1.0;
printf("%f", (float)d).
Оказывается, правильно делал :-)

Дополнение к третьему примеру

Если вдруг у кого-то после прочтения заметки возникнет вопрос, а как же, собственно, прочитать double, подсказываю:

double theDouble;
scanf("%lf" ....