Статья: Переход от С к С++
Статья: Переход от С к С++
Малышев Сергей Михайлович
Хочу
сразу же сказать, что эта статья отнюдь не претендует на полные и
безоговорочные рекомендации по переходу от С к С++. Тут даны лишь некоторые из
очень многочисленных и может быть наиболее распространенные из них. Итак, к
делу...
Для
того чтобы освоиться с C++, необходимо некоторое время. Поскольку С является, по
существу, подмножеством C++, все его старые "трюки" остаются в силе, но
многие из них теряют свою значимость. Так, например, для программистов на C++
выражение "указатель на указатель" звучит немного забавно. Почему
вместо указателя не была использована просто ссылка?
С
- достаточно простой язык. Макросы, указатели, структуры, массивы и функции - это
почти все, что он в действительности предлагает. Каким бы сложным ни оказался
алгоритм, его всегда можно реализовать, используя перечисленный набор средств.
В
C++ дело обстоит несколько иначе: наравне с макросами, указателями, структурами,
массивами и функциями используются закрытые и защищенные члены классов, перегрузка
функций, аргументы по умолчанию, конструкторы и деструкторы. Операции, определяемые
пользователем, встроенные функции, ссылки, дружественные классы и функции, шаблоны,
исключения, пространства имен и т. д. Очевидно, что более богатые средства
проектирования предоставляют и более широкие возможности, а это в свою очередь,
требует существенно иной культуры программирования.
Столкнувшись
с таким разнообразием выбора, многие теряются, продолжая крепко держаться за то,
к чему они привыкли. По большей части в этом нет особого греха, но некоторые
"привычки" из С идут вразрез с духом C++. От них просто необходимо
избавиться!
Давайте
рассмотрим две наиболее частые и стойкие (на мой взгляд и собственный опыт)
"привычки" из С - это использование директивы #define и функций
scanf/printf.
Предпочитайте
const и inline использованию #define
Этот
правило лучше было бы назвать "Компилятор предпочтительнее
препроцессора", поскольку #define зачастую вообще не относят к языку C++.
В этом и заключается одна из проблем. Рассмотрим простой пример; попробуйте
написать что-нибудь вроде: #define ASPECT_RATIO 1.653
Символическое
обозначение может так и остаться неизвестным компилятору или быть удалено
препроцессором, прежде чем код попадет в компилятор. Если это произойдет, то
обозначение ASPECT_RATIO не окажется в таблице символов. Поэтому в ходе
компиляции вы получите ошибку, связанную с использованием константы (в
сообщении об ошибке будет сказано 1.653, а не ASPECT_RATIO).
Это
вызовет путаницу. Если файл заголовков писали не вы, а кто-либо другой, у вас
не будет никакого представления о том, откуда взялось значение 1.653, и на
поиски ответа вы потеряете много времени. Та же проблема может возникнуть и при
отладке, поскольку обозначение, выбранное вами, будет отсутствовать в таблице
символов.
Указанная
задача решается просто и быстро. Вместо использования макроса препроцессора
определите константу: const double ASPECT_RATIO = 1.653;
Однако
есть два специальных случая, заслуживающих упоминания. Во-первых, при
определении константных указателей могут возникнуть некоторые осложнения.
Поскольку определения констант обычно выносятся в заголовочные файлы (где к ним
получает доступ множество различных исходных файлов), важно, чтобы сам
указатель был объявлен с const, в дополнение к объявлению const того, на что он
указывает. Например, для определения в файле заголовков константной строки
char* следует писать const дважды: const char* const constantString =
"String is constant";
Во-вторых,
иногда удобно определять некоторые константы, как относящиеся к конкретным
классам, а это требует другого подхода. Для того чтобы ограничить область
действия константы конкретным классом, необходимо сделать ее членом этого
класса, а чтобы гарантировать, что существует только одна копия константы, требуется
сделать ее статическим (static) членом класса:
class GamePlayer
{
private:
static const int NUM_TURNS = 5; // Объявление константы
int
scores[NUM_TURNS]; // Использование константы
};
Остается
еще одна небольшая проблема, поскольку все то, что вы видите выше - это
объявление, а не определение NUM_TURNS. Если вам необходимо определить
статические члены класса в файле реализации, то напишите следующее:
сonst
int GamePlayer::NUM_TURNS; // Обязательное объявление
//
находится в файле реализации
Впрочем,
терять сон из-за подобных пустяков не стоит. Если об определении забудете вы, то
напомнит компоновщик.
Старые
компиляторы могут не поддерживать принятый здесь синтаксис, так как в более
ранних версиях языка было запрещено задавать значения статических членов класса
во время их объявления. Более того, инициализация в классе допускалась только
для целых типов (таких как int, bool, char и пр.) и для констант. Если
вышеприведенный синтаксис не работает, то начальное значение следует задавать в
определении:
class
EngineeringConstants // Это находится в файле заголовка класса.
{
private:
static const double FUDGE_FACTOR;
};
А
это находится в файле реализации класса: const double
EngineeringConstants::FUDGE_FACTOR = 1.35;
Единственное
исключение обнаруживается тогда, когда для компиляции класса необходима
константа. Например, при объявлении массива GamePlayer::scores в листинге, приведенном
выше, в момент компиляции может потребоваться задание его размера. Для того
чтобы работать с компилятором, ошибочно запрещающим инициализировать целые
константы внутри класса, следует применять технику, которая иногда называется
"трюком с перечислением". Она основана на том, что переменные
перечисляемого типа можно использовать там, где ожидаются целые числа, поэтому
GamePlayer определяют следующим образом:
class
GamePlayer
{
private:
enum
{ NUM_TURNS = 5 }; //трюк с перечислением - делает из
//NUM_TURNS
символ со значением 5
int scores[NUM_TURNS]; //теперь нормально
};
Если
вы имеете дело не с примитивным компилятором, написанным до 1995 года и
представляющим собой только исторический интерес, то считайте, что вам скорее
всего повезло: необходимость использовать этот трюк вероятно отпадет сама
собой. Вернемся к препроцессору. Другой частый случай неправильного
использования директивы #define - создание макросов, которые выглядят как
функции, но не обременены накладными расходами функционального вызова.
Канонический пример - вычисление максимума двух значений: #define max(a, b)
((a)>(b)?(а):(b))
В
этой небольшой строчке содержится так много недостатков, что даже не совсем
понятно, с какого проще начать. Всякий раз, когда вы пишете макрос подобный
этому, необходимо помнить, что все аргументы следует заключать в скобки. В
противном случае у пользователей будут возникать серьезные проблемы с
применением таких макросов в выражениях. Но, даже если вы все сделаете верно, посмотрите,
какие странные (если не сказать - страшные) вещи могут при этом произойти:
int
а = 5, b = 0;
mах(++а,
b); //здесь переменная "а" - увеличивается дважды
mах(++а,
b+10); //а здесь - только 1 раз и это правильно
Происходящее
внутри mах зависит от того, что с чем сравнивается! Не верится? Проверьте сами.
К счастью, вам нет нужды мириться с поведением, так сильно противоречащим
привычной логике. Существует метод, позволяющий добиться такой же эффективности,
как при использовании макросов. В таком случае обеспечиваются как
предсказуемость поведения, так и контроль типов аргументов (что характерно для
обычных функций). Этот результат достигается применением встраиваемых функций:
inline
int max(int a, int b) { return a > b ? a : b; }
Новая
версия max несколько отличается от предыдущей, поскольку она может работать
только с целыми аргументами. Возникшую проблему удачно решает шаблон:
template
inline
const Т& max(const Т& а, const T& b)
{
return а > b ? а : b; }
Он
генерирует целое семейство функций, каждая из которых берет два приводимых к одному
типу объекта и возвращает ссылку (с модификатором const) на больший из двух
объектов. Поскольку вам неизвестно, каким будет тип Т, для эффективности
передача и возврат значения происходят по ссылке.
Кстати
говоря, прежде чем вы решите писать шаблон для какой-либо функции, подобной max,
узнайте, не присутствует ли она уже в стандартной библиотеке. В случае с max вы
можете воспользоваться плодами чужих усилий: max является частью стандартной
библиотеки C++.
Предпочитайте
использованию
О
эти операторы sсanf() и printf()! Практически все формы обучения языку С и
(увы) С++ начинаются с них. Да, они переносимы. Да, они эффективны. Да, вы уже
даже знаете, как их нужно использовать. Но какой бы благоговейный восторг они
ни вызывали, факт остается фактом: операторы sсanf() и printf() и им подобные
далеки от совершенства. В частности, они не осуществляют контроль типа
переменной и к тому же нерасширяемы.
Поскольку
контроль типов и расширяемость - краеугольные камни идеологии C++, то лучше
всего с самого начала во всем опираться на них. Кроме того, семейство функций
printf/scanf отделяет переменные, которые необходимо прочитать или записать, от
форматирующей информации, управляющей записью и чтением. Неудивительно, что эти
слабости функции printf/scanf - сила операторов " и ".
int
i;
Rational
r; // r является рациональным числом (класс Rational).
cin " i " r;
cout " i " r;
Если
этот код предназначен для компиляции, должны быть в наличии функции
operator" и operator", которые могли бы работать с объектом типа Rational.
Отсутствие данных функций является ошибкой. (Для int и других стандартных типов
имеются стандартные версии этих операторов.)
Страницы: 1, 2 |