Статья: Переход от С к С++
Статья: Переход от С к С++
Малышев Сергей Михайлович
Хочу
сразу же сказать, что эта статья отнюдь не претендует на полные и
безоговорочные рекомендации по переходу от С к С++. Тут даны лишь некоторые из
очень многочисленных и может быть наиболее распространенные из них. Итак, к
делу...
Для
того чтобы освоиться с 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 и других стандартных типов
имеются стандартные версии этих операторов.)
Более
того, компилятор берет на себя заботу о том, какие версии операторов вызывать
для разных переменных. Таким образом, вам нет необходимости беспокоиться о том,
что первый читаемый или записываемый объект имеет тип int, а второй - Rational
- с этим разберется компилятор!
Кроме
того, считывание объектов происходит с использованием той же синтаксической
формы, что и при записи. Поэтому нет необходимости помнить о том, что, если вы
работаете не с указателем, важно не забыть взять адрес, а если имеете дело с
указателем, следует убедиться, что вы не берете адрес. Пусть о таких деталях
заботится компилятор C++. Это его дело. У вас же есть задачи посерьезнее.
И
наконец, заметьте, что встроенные типы, подобные int, читаются и записываются
совершенно аналогично типам, определенным пользователями, таким, например, как
Rational. Попробуйте сделать то же самое, используя scanf/printf!
Ниже
приводится пример того, как можно написать функцию для вывода класса
рациональных чисел:
class Rational
{
public:
Rational(int numerator=0, int denominator=1);
int
n, d; //числитель и знаменатель
friend ostream& operator"(ostream& s, const Rational&
r);
};
ostream& operator"(ostream& s, const Rational& r)
{
s " r.n " '/' " r.d;
return s;
}
Эта
версия operator" демонстрирует некоторые тонкости (притом весьма важные!).
Например, она не является функцией-членом, а объект Rational передается
operator" по ссылке const, а не как объект. Соответствующая функция ввода,
operator", объявляется и реализуется аналогичным образом.
Как
ни обидно это признавать, в ряде случаев имеет смысл вернуться к старому и
проверенному способу. Во-первых, некоторые реализации операций потоков
ввода/вывода менее эффективны, чем соответствующие операции С, и возможно (хотя
маловероятно), что в отдельных приложениях это может оказаться существенным.
Помните, однако: это относится не к потокам ввода/вывода вообще, а только к той
или иной реализации. Во-вторых, библиотека потоков ввода/вывода, в ходе своей
стандартизации претерпела ряд кардинальных изменений. Следовательно, приложения,
требующие максимальной переносимости, могут столкнуться с тем фактом, что
различные поставщики поддерживают различные приближения стандарта.
И
наконец, поскольку классы библиотеки потоков ввода/вывода имеют конструкторы, а
функции <stdio.h> - нет, в редких случаях существенным будет порядок
инициализации статических объектов, и стандартная библиотека С окажется более
удобной просто потому, что вы можете ею пользоваться без опасений.
Контроль
типов и расширяемость, предлагаемые классами и функциями библиотеки потоков
ввода/вывода, являются более важными, чем это может показаться, - не стоит
отвергать их только из-за того, что вы привыкли к <stdio.h>.
Между
прочим, это не опечатка - в названии данного правила действительно фигурирует
<iostream>, а не <iostream.h>. Строго говоря, такого заголовка, как
<iostream.h>, не существует: Комитет по стандартам отказался от него в
пользу названия <iostream> при сокращении имен стандартных файлов
заголовков, отсутствующих в библиотеке языка С.
Важно
уяснить следующее: если (что весьма вероятно) ваш компилятор поддерживает как
файл заголовков <iostream>, так и <iostream.h>, необходимо иметь в
виду, что они слегка отличаются друг от друга. В частности, если вы включаете
<iostream>, элементы библиотеки потоков ввода/вывода весьма удобно
расположены в пространстве имен std; включая <iostream.h>, вы получаете
те же элементы, но в глобальном пространстве имен. Их определение в нем может
вести к конфликтам, предотвращению которых и должно было послужить введение
понятия пространства имен. Кроме того, <iostream> короче, чем
<iostream.h>.
Для
многих это оказывается достаточным аргументом в пользу нового названия. Вот на
этом пока и все. Если будут вопросы - пишите. По результатам вашего любопытства
могут появиться новые статьи.
При
написании данного текста активно использовалась книга Скотта Мейерса.
Список литературы
Scott Meyers Effective C++ Second Edition AWG 1998
Для
подготовки данной работы были использованы материалы с сайта http://my-pc.jino.ru/
|