Продолжая использовать наш сайт, вы даете согласие на обработку файлов cookie, которые обеспечивают правильную работу сайта. Благодаря им мы улучшаем сайт!
Принять и закрыть

Читать, слущать книги онлайн бесплатно!

Электронная Литература.

Бесплатная онлайн библиотека.

Читать: Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс на бесплатной онлайн библиотеке Э-Лит


Помоги проекту - поделись книгой:

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

Сегодня C++ – это язык программирования с несколькими парадигмами, поддерживающий процедурное, объектно-ориентированное, функциональное, обобщенное и метапрограммирование. Эти мощь и гибкость делают C++ несравненным инструментом, однако могут привести в замешательство. У любой рекомендации по «правильному применению» есть исключения. Как найти смысл в таком языке?

Лучше всего воспринимать C++ не как один язык, а как конгломерат взаимосвязанных языков. В пределах отдельного подъязыка правила достаточно просты, понятны и легко запоминаются. Однако когда вы переходите от одного подъязыка к другому, правила могут изменяться. Чтобы увидеть смысл в C++, вы должны распознавать его основные подъязыки. К счастью, их всего четыре:

• C. В глубине своей C++ все еще основан на C. Блоки, предложения, препроцессор, встроенные типы данных, массивы, указатели и т. п. – все это пришло из C. Во многих случаях C++ предоставляет для решения тех или иных задач более развитые механизмы, чем C (пример см. в правиле 2 – альтернатива препроцессору и 13 – применение объектов для управления ресурсами), но когда вы начнете работать с той частью C++, которая имеет аналоги в C, то поймете, что правила эффективного программирования отражают более ограниченный характер языка C: никаких шаблонов, никаких исключений, никакой перегрузки и т. д.

Объектно-ориентированный C++. Эта часть C++ представляет то, чем был «C с классами», включая конструкторы и деструкторы, инкапсуляцию, наследование, полиморфизм, виртуальные функции (динамическое связывание) и т. д. Это та часть C++, к которой в наибольшей степени применимы классические правила объектно-ориентированного проектирования.

C++ с шаблонами. Эта часть C++ называется обобщенным программированием, о ней большинство программистов знают мало. Шаблоны теперь пронизывают C++ снизу доверху, и признаком хорошего тона в программировании уже стало включение конструкций, немыслимых без шаблонов (например, см. правило 46 о преобразовании типов при вызовах шаблонных функций). Фактически шаблоны, благодаря своей мощи, породили совершенно новую парадигму программирования: метапрограммирование шаблонов (template metaprogramming – TMP). В правиле 48 представлен обзор TMP, но если вы не являетесь убежденным фанатиком шаблонов, у вас нет причин чрезмерно задумываться об этом. TMP не отнесешь к самым распространенным приемам программирования на C++.

STL. STL – это, конечно, библиотека шаблонов, но очень специализированная. Принятые в ней соглашения относительно контейнеров, итераторов, алгоритмов и функциональных объектов великолепно сочетаются между собой, но шаблоны и библиотеки можно строить и по-другому. Работая с библиотекой STL, вы обязаны следовать ее соглашениям.

Помните об этих четырех подъязыках и не удивляйтесь, если попадете в ситуацию, когда соображения эффективности программирования потребуют от вас менять стратегию при переключении с одного подъязыка на другой. Например, для встроенных типов (в стиле C) передача параметров по значению в общем случае более эффективна, чем передача по ссылке, но если вы программируете в объектно-ориентированном стиле, то из-за наличия определенных пользователем конструкторов и деструкторов передача по ссылке на константу обычно становится более эффективной. В особенности это относится к подъязыку «C++ с шаблонами», потому что там вы обычно даже не знаете заранее типа объектов, с которыми имеете дело. Но вот вы перешли к использованию STL, и опять старое правило C о передаче по значению становится актуальным, потому что итераторы и функциональные объекты смоделированы через указатели C. (Подробно о выборе способа передачи параметров см. правило 20.)

Таким образом, C++ не является однородным языком с единственным набором правил. Это – конгломерат подъязыков, каждый со своими собственными соглашениями. Если вы будете помнить об этих подъязыках, то обнаружите, что понять C++ намного проще.

Что следует помнить

• Правила эффективного программирования меняются в зависимости от части C++, которую вы используете.

Правило 2: Предпочитайте const, enum и inline использованию #define

Это правило лучше было бы назвать «Компилятор предпочтительнее препроцессора», поскольку #define зачастую вообще не относят к языку C++. В этом и заключается проблема. Рассмотрим простой пример; попробуйте написать что-нибудь вроде:

#define ASPECT_RATIO 1.653

Символическое имя ASPECT_RATIO может так и остаться неизвестным компилятору или быть удалено препроцессором до того, как код поступит на обработку компилятору. Если это произойдет, то имя ASPECT_RATIO не попадет в таблицу символов. Поэтому в ходе компиляции вы получите ошибку (в сообщении о ней будет упомянуто значение 1.653, а не ASPECT_RATIO). Это вызовет путаницу. Если имя ASPECT_RATIO было определено в заголовочном файле, который писали не вы, то вы вообще не будете знать, откуда взялось значение 1.653, и на поиски ответа потратите много времени. Та же проблема может возникнуть и при отладке, поскольку выбранное вами имя будет отсутствовать в таблице символов.

Решение состоит в замене макроса константой:

const double AspectRatio = 1.653; // имена, записанные большими буквами,

// обычно применяются для макросов,

// поэтому мы решили его изменить

Будучи языковой константой, AspectRatio видима компилятору и, естественно, помещается в таблицу символов. К тому же в случае использования константы с плавающей точкой (как в этом примере) генерируется более компактный код, чем при использовании #define. Дело в том, что препроцессор, слепо подставляя вместо макроса ASPECT_RATIO величину 1.653, создает множество копий 1.653 в объектном коде, в то время как использование константы никогда не породит более одной копии этого значения.

При замене #define константами нужно помнить о двух особых случаях. Первый касается константных указателей. Поскольку определения констант обычно помещаются в заголовочные файлы (где к ним получает доступ множество различных исходных файлов), важно, чтобы сам указатель был объявлен с ключевым словом const, в дополнение к объявлению const того, на что он указывает. Например, чтобы объявить в заголовочном файле константную строку типа char*, слово const нужно написать дважды:

const char * const authorName = “Scott Meyers”;

Более подробно о сущности и применений слова const, особенно в связке с указателями, см. в правиле 3. Но уже сейчас стоит напомнить, что объекты типа string обычно предпочтительнее своих прародителей – строк типа char *, поэтому authorName лучше определить так:

const std::string authorName(“Scott Meyers”);

Второе замечание касается констант, объявляемых в составе класса. Чтобы ограничить область действия константы классом, необходимо сделать ее членом класса, и чтобы гарантировать, что существует только одна копия константы, требуется сделать ее статическим членом:

class GamePlayer {

private:

static const int NumTurns = 5; // объявление константы

int scores[NumTurns]; // использование константы

...

};

То, что вы видите выше, – это объявление NumTurns, а не ее определение. Обычно C++ требует, чтобы вы представляли определение для всего, что используете, но объявленные в классе константы, которые являются статическими и имеют встроенный тип (то есть целые, символьные, булевские) – это исключение из правил. До тех пор пока вы не пытаетесь получить адрес такой константы, можете объявлять и использовать ее без предоставления определения. Если же вам нужно получить адрес либо если ваш компилятор настаивает на наличии определения, то можете написать что-то подобное:

const int GamePlayer::NumTurns; // определение NumTurns; см. ниже,

// почему не указывается значение

Поместите этот код в файл реализации, а не в заголовочный файл. Поскольку начальное значение константы класса представлено там, где она объявлена (то есть NumTurns инициализировано значением 5 при объявлении), то в точке определения задавать начальное значение не требуется.

Отметим, кстати, что нет возможности объявить в классе константу посредством #define, потому что #define не учитывает области действия. Как только макрос определен, он остается в силе для всей оставшейся части компилируемого кода (если только где-то ниже не встретится #undef). Это значит, что директива #define неприменима не только для объявления констант в классе, но вообще не может быть использована для обеспечения какой бы то ни было инкапсуляции, то есть придать смысл выражению «private #define» невозможно. В то же время константные данные-члены могут быть инкапсулированы, примером может служить NumTurns.

Старые компиляторы могут не поддерживать показанный выше синтаксис, так как в более ранних версиях языка было запрещено задавать значения статических членов класса во время объявления. Более того, инициализация в классе допускалась только для целых типов и для констант. Если вышеприведенный синтаксис не работает, то начальное значение следует задавать в определении:

class CostEstimate {

private:

static const double FudgeFactor; // объявление статической константы

... // класса – помещается в файл заголовка

};

const double // определение статической константы

CostEstimate::FudgeFactor = 1.35; // класса – помещается в файл реализации

Обычно ничего больше и не требуется. Единственное исключение обнаруживается тогда, когда для компиляции класса необходима константа. Например, при объявлении массива GamePlayer::scores компилятору нужно знать размер массива. Чтобы работать с компилятором, ошибочно запрещающим инициализировать статические целые константы внутри класса, можно воспользоваться способом, известным под названием «трюка с перечислением». Он основан на том, что переменные перечисляемого типа можно использовать там, где ожидаются значения типа int, поэтому GamePlayer можно определить так:

class GamePlayer {

private:

enum ( NumTurns = 5 }; // “трюк с перечислением” – делает из

// NumTurns символ со значением 5

int scores[NumTurns]; // нормально

...

};

Этот прием стоит знать по нескольким причинам. Во-первых, поведение «трюка с перечислением» в некоторых отношениях более похоже на #define, чем на константу, а иногда это как раз то, что нужно. Например, можно получить адрес константы, но нельзя получить адрес перечисления, как нельзя получить и адрес #define. Если вы хотите запретить получать адрес или ссылку на какую-нибудь целую константу, то применение enum – хороший способ наложить такое ограничение. (Подробнее о поддержке проектных ограничений с помощью приемов кодирования можно узнать из правила 18). К тому же, хотя хорошие компиляторы не выделяют память для константных объектов целых типов (если только вы не создаете указателя или ссылки на объект), менее изощренные могут так поступать, а вам это, возможно, ни к чему. Как и #define, перечисления никогда не станут причиной подобного нежелательного распределения памяти.

Вторая причина знать о «трюке с перечислением» чисто прагматическая. Он используется в очень многих программах, поэтому нужно уметь распознавать этот трюк, когда вы с ним сталкиваетесь. Вообще говоря, этот прием – фундаментальная техника, применяемая при метапрограммировании шаблонов (см. правило 48).

Вернемся к препроцессору. Другой частый случай неправильного использования директивы #define – создание макросов, которые выглядят как функции, но не обременены накладными расходов, связанными с вызовом функций. Ниже представлен макрос, который вызывает некоторую функцию f c аргументом, равным максимальному из двух значений:

// вызвать f, передав ей максимум из a и b

#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))

В этой строчке содержится так много недостатков, что даже не совсем понятно, с какого начать.

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

int a = 5, b = 0;

CALL_WITH_MAX(++a, b); // a увеличивается дважды

CALL_WITH_MAX(++a, b+10); // a увеличивается один раз

Происходящее внутри max зависит от того, с чем она сравнивается!

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

template <typename T>

inline void callWithMax(const T& a, const T& b) // Поскольку мы не знаем,

{ // что есть T, то передаем

f(a > b ? a : b); // его по ссылке на const -

} // см. параграф 20

Этот шаблон генерирует целое семейство функций, каждая из которых принимает два аргумента одного и того же типа и вызывает f с наибольшим из них. Нет необходимости заключать параметры в скобки внутри тела функции, не нужно заботиться о многократном вычислении параметров и т. д. Более того, поскольку callWithMax – настоящая функция, на нее распространяются правила областей действия и контроля доступа. Например, можно говорить о встроенной функции, являющейся закрытым членом класса. Описать нечто подобное с помощью макроса невозможно.

Наличие const, enum и inline резко снижает потребность в препроцессоре (особенно это относится к #define), но не устраняет ее полностью. Директива #include остается существенной, а #ifdef/#ifndef продолжают играть важную роль в управлении компиляцией. Пока еще не время отказываться от препроцессора, но определенно стоит задуматься, как избавиться от него в дальнейшем.

Что следует помнить

• Для простых констант директиве #define следует предпочесть константные объекты и перечисления (enum).

• Вместо имитирующих функции макросов, определенных через #define, лучше применять встроенные функции.

Правило 3: Везде, где только можно используйте const

Замечательное свойство модификатора const состоит в том, что он накладывает определенное семантическое ограничение: данный объект не должен модифицироваться, – и компилятор будет проводить это ограничение в жизнь. const позволяет указать компилятору и программистам, что определенная величина должна оставаться неизменной. Во всех подобных случаях вы должны обозначить это явным образом, призывая себе на помощь компилятор и гарантируя тем самым, что ограничение не будет нарушено.

Ключевое слово const удивительно многосторонне. Вне классов вы можете использовать его для определения констант в глобальной области или в пространстве имен (см. правило 2), а также для статических объектов (внутри файла, функции или блока). Внутри классов допустимо применять его как для статических, так и для нестатических данных-членов. Для указателей можно специфицировать, должен ли быть константным сам указатель, данные, на которые он указывает, либо и то, и другое (или ни то, ни другое):



Поделиться книгой:

На главную
Назад