explicit C(int x); // это не конструктор по
// умолчанию
};
Конструкторы классов B и C объявлены в ключевым словом explicit (явный). Это предотвращает их использование для неявных преобразований типов, хотя не запрещает применения, если преобразование указано явно:
void doSomething(B bObject); // функция принимает объект типа B
B bObj1; // объект типа B
doSomething(bObj1); // нормально, B передается doSomething
B bObj(28); // нормально, создает B из целого 28
// (параметр bool по умолчанию true)
doSomething(28); // ошибка! doSomething принимает B,
// а не int, и не существует неявного
// преобразования из int в B
doSomething(B(28)); // нормально, используется конструктор
// B для явного преобразования (приведения)
// int в B (см. в правиле 27 информацию
// о приведении типов)
Конструкторы, объявленные как explicit, обычно более предпочтительны, потому что предотвращают выполнение компиляторами неявных преобразований типа (часто нежелательных). Если нет основательной причины для использования конструкторов в неявных преобразованиях типов, я всегда объявляю их explicit. Советую и вам придерживаться того же принципа.
Обратите внимание, что в предшествующем примере приведение выделено. Я и дальше буду использовать такое выделение, чтобы подчеркнуть важность излагаемого материала. (Также я выделяю номера глав, но это только потому, что мне кажется, это выглядит симпатично.)
Конструктор копирования (copy constructor) используется для инициализации объекта значением другого объекта того же самого типа, а копирующий оператор присваивания (copy assignment operator) применяется для копирования значения одного объекта в другой – того же типа:
class Widget {
public:
Widget(); // конструктор по умолчанию
Widget(const Widget& rhs); // конструктор копирования
Widget& operator=(const Widget& rhs); // копирующий оператор присваивания
...
};
Widget w1; // вызов конструктора по умолчанию
Widget w2(w1); // вызов конструктора копирования
w1 = w2; // вызов оператора присваивания
// копированием
Будьте внимательны, когда видите конструкцию, похожую на присваивание, потому что синтаксис «=» также может быть использован для вызова конструктора копирования:
Widget w3 = w2; // вызов конструктора копирования!
К счастью, конструктор копирования легко отличить от присваивания. Если новый объект определяется (как w3 в последнем предложении), то должен вызываться конструктор, это не может быть присваивание. Если же никакого нового объекта не создается (как в «w1=w2»), то конструктор не применяется и это – присваивание.
Конструктор копирования – особенно важная функция, потому что она определяет, как объект передается по значению. Например, рассмотрим следующий фрагмент:
bool hasAcceptableQuality(Widget w);
...
Widget aWidget;
if (hasAcceptableQuality(aWidget)) ...
Параметр w передается функции hasAcceptableQuality по значению, поэтому в приведенном примере вызова aWidget копируется в w. Копирование осуществляется конструктором копирования из класса Widget. Вообще передача по значению
STL – стандартная библиотека шаблонов (Standard Template Library) – это часть стандартной библиотеки, касающаяся контейнеров (то есть vector, list, set, map и т. д.), итераторов (то есть vector<int>::iterator, set<string>::iterator и т. д.), алгоритмов (то есть for_each, find, sort и т. д.) и всей связанной с этим функциональности. В ней очень широко используются объекты-функции (function objects), то есть объекты, ведущие себя подобно функциям. Такие объекты представлены классами, в которых перегружен оператор вызова operator(). Если вы не знакомы с STL, вам понадобится, помимо настоящей книги, какое-нибудь достойное руководство, посвященное этой теме, ведь библиотека STL настолько удобна, что не воспользоваться ее преимуществами было бы непростительно. Стоит лишь начать работать с ней, и вы сами это почувствуете.
Программистам, пришедшим к C++ от языков вроде Java или C#, может показаться странным понятие неопределенного поведения. По различным причинам поведение некоторых конструкций в C++ действительно не определено: вы не можете уверенно предсказать, что произойдет во время исполнения. Вот два примера такого рода:
int *p = 0; // p – нулевой указатель
std::cout << *p; // разыменование нулевого указателя
char name[] = “Daria” // name – массив длины 6 (не забудьте про
// завершающий нуль!)
char c = name[10]; // указание неправильного индекса массива
// порождает неопределенное поведение
Дабы подчеркнуть, что результаты неопределенного поведения невозможно предсказать и что они могут быть весьма неприятны, опытные программисты на C++ часто говорят, что программы с неопределенным поведением могут стереть содержимое жесткого диска. Это правда: такая программа
Иной термин, который может смутить программистов, пришедших из других языков, – это интерфейс. В Java и. NET-совместимых языках интерфейсы являются частью языка, но в C++ ничего подобного нет, хотя в правиле 31 рассматривается некоторое приближение. Когда я использую термин «интерфейс», то обычно имею в виду сигнатуры функций, доступные члены класса («открытый интерфейс», «защищенный интерфейс», «закрытый интерфейс») или выражения, допустимые в качестве параметров типа для шаблонов (см. правило 41). То есть под интерфейсом я понимаю общую концепцию проектирования.
Понятие клиент – это нечто или некто, использующий написанный вами код (обычно через интерфейсы). Так, например, клиентами функции являются ее пользователи: части кода, которые вызывают функцию (или берут ее адрес), а также люди, которые пишут и сопровождают такой код. Клиентами класса или шаблона являются части программы, использующие этот класс или шаблон, а равно программисты, которые пишут или сопровождают эти части. Когда речь заходит о клиентах, я обычно имею в виду программистов, поскольку именно они могут быть введены в заблуждение или недовольство плохо разработанным интерфейсом. Коду, который они пишут, такие эмоции недоступны.
Возможно, вы не привыкли думать о клиентах, но я постараюсь убедить вас в необходимости облегчить им жизнь, насколько это возможно. В конце концов, вы сами – клиент программного обеспечения, которое разрабатывал кто-то другой. Ведь вы хотели бы, чтоб его авторы облегчили вам работу? Помимо того, рано или поздно вы окажетесь в положении, когда сами станете клиентом собственного кода (то есть будете использовать код, написанный вами), и тогда оцените, что при разработке интерфейсов нужно помнить об интересах клиентов.
В этой книге я часто обращаю внимание на различие между функциями и шаблонами функций, а также между классами и шаблонами классов. Это не случайно, ведь то, что справедливо для одного, часто справедливо и для другого. В ситуациях, когда это не так, я делаю различие между классами, функциями и шаблонами, из которых порождаются классы и функции.
Соглашения об именах
Я пытался выбирать осмысленные имена для объектов, классов, функций, шаблонов и т. п., но семантика некоторых придуманных мной имен может быть для вас неочевидна. Например, я часто использую для параметров имена lhs и rhs. Имеется в виду соответственно «левая часть» (left-hand side) и «правая часть» (right-hand side). Эти имена обычно употребляются в функциях, реализующих бинарные операторы, то есть operator== и operator*. Например, если a и b – объекты, представляющие рациональные числа, и если объекты класса Rational можно перемножать с помощью функции-нечлена operator*() (подобный случай описан в правиле 24), то выражение
a*b
эквивалентно вызову функции:
operator*(a, b);
В правиле 24 я объявляю operator* следующим образом:
const Rational operator*(const Rational& lhs, const Rational& rhs);
Как видите, левый операнд – a – внутри функции называется lhs, а правый – b – rhs.
Для функций-членов аргумент в левой части оператора представлен указателем this, а единственный оставшийся параметр я иногда называю rhs. Возможно, вы заметили это в объявлении некоторых функций-членов класса Widget в примерах выше. «Widget» не значит ничего. Это просто имя, которое я иногда использую для того, чтобы как-то назвать пример класса. Оно не имеет никакого отношения к элементам управления (виджетам), применяемым в графических интерфейсах (GUI).
Часто я именую указатели, следуя соглашению, с соответствии с которым указатель на объект типа T называется pt («pointer to T»). Вот некоторые примеры:
Widget *pw; // pw = указатель на Widget
class Airplane;
Airplane *pa; // pa = указатель на Airplane
class GameCharacter;
GameCharacter *pgc; // pgc = указатель на GameCharacter
Похожее соглашение применяется и для ссылок: rw может быть ссылкой на Widget, а ra – ссылкой на Airplane.
Иногда для именования функции-члена я использую имя mf.
Многопоточность
В самом языке C++ нет представления о потоках (threads), да и вообще о каких-либо механизмах параллельного исполнения. То же относится и к стандартной библиотеке C++. Иными словами, с точки зрения C++ многопоточных программ не существует.
Однако они есть. Хотя в этой книге я буду говорить преимущественно о стандартном, переносимом C++, но невозможно игнорировать тот факт, что безопасность относительно потоков – требование, с которым сталкиваются многие программисты. Признавая этот конфликт между стандартным C++ и реальностью, я буду отмечать те случаи, когда рассматриваемые конструкции могут вызвать проблемы при работе в многопоточной среде. Не надо думать, что эта книга научит вас многопоточному программированию на C++. Вовсе нет. Я рассматривал главным образом однопоточные приложения, но не игнорировал существование многопоточности и старался отмечать те случаи, когда программисты, пишущие многопоточные программы, должны следовать моим советам с осторожностью.
Если вы не знакомы с концепцией многопоточности и не интересуетесь этой темой, то можете не обращать внимания на относящиеся к ней замечания. В противном случае имейте в виду, что мои комментарии – не более, чем скромный намек на то, что необходимо знать, если вы собираетесь использовать C++ для написания многопоточных программ.
Библиотеки TR1 и Boost
Ссылки на библиотеки TR1 и Boost вы будете встречать на протяжении всей этой книги. Каждой из них посвящено отдельное правило (54 – TR1 и 55 – Boost), но, к сожалению, они находятся в самом конце книги. При желании можете прочесть их прямо сейчас, но если вы предпочитаете читать книгу по порядку, а не с конца, то следующие замечания помогут понять, о чем идет речь:
• TR1 ("Technical Report 1") – это спецификация новой функциональности, добавленной в стандартную библиотеку C++. Она оформлена в виде новых шаблонов классов и функций, предназначенных для реализации хэш-таблиц, «интеллектуальных» указателей с подсчетом ссылок, регулярных выражений и многого другого. Все компоненты TR1 находятся в пространстве имен tr1, которое вложено в пространство имен std.
• Boost – это организация и Web-сайт (http://boost.org), на котором предлагаются переносимые, тщательно проверенные библиотеки C++ с открытым исходным кодом. Большая часть TR1 базируется на работе, выполненной Boost, и до тех пор, пока поставщики компиляторов не включат TR1 в дистрибутивы C++, Web-сайт Boost будет оставаться для разработчиков главным источником реализаций TR1. Boost предоставляет больше, чем включено в TR1, однако в любом случае о нем полезно знать.
Глава 1
Приучайтесь к C++
Независимо от опыта программирования, для того чтобы освоиться с C++, потребуется некоторое время. Это мощный язык с очень широким диапазоном возможностей, но чтобы использовать их эффективно, нужно несколько изменить свой способ мышления. Книга как раз и призвана помочь вам в этом, но какие-то вопросы являются более важными, какие-то – менее, а эта глава посвящна самым важным вещам.
Правило 1: Относитесь к C++ как к конгломерату языков
Поначалу C++ был просто языком C с добавлением некоторых объектно-ориентированных средств. Даже первоначальное название C++ («C с классами») отражает эту связь.