Краткий вопросник по C++. Часть 2 (FAQ)Источник: hardline Автором английской версии является Marshall Cline (cline@parashift.com), автором перевода - Ярослав Миронов (slava_mironov@mail.ru)
[9.4] Что сделать, чтобы определить функцию - не член класса как встроенную? Когда вы объявляете встроенную функцию, это выглядит как обычное объявление функции: void f(int i, char c); Но перед определением встроенной функции пишется слово inline , и само определение помещается в заголовочный файл: inline void f(int i, char c) { // ... } Примечание: Необходимо, чтобы определение встроенной функции (часть между { ... } ) была помещена в заголовочный файл, за исключением того случая, когда функция используется только в одном .cpp файле. Если вы помещаете определение встроенной функции в .cpp файл, а вызываете ее из другого .cpp файла, то вы получаете ошибку "unresolved external" ("ненайденный внешний объект") от компоновщика (linker). (Примечание переводчика: На всякий случай уточню, что само помещение определения функции в заголовочный файл НЕ делает ее встроенной. Это требуется только для того, чтобы тело функции было видно во всех местах, где она вызывается. Иначе невозможно обеспечить встраивание функции. - YM) [9.5] Как сделать встроенной функцию - член класса? Когда вы объявляете встроенную функцию - член класса, это выглядит как обычное объявление функции - члена: class Fred { public: void f(int i, char c); }; Но когда перед определением встроенной функции пишется слово inline , а само определение помещается в заголовочный файл: inline void Fred::f(int i, char c) { // ... } Примечание: Необходимо, чтобы определение встроенной функции (часть между {...} ) была помещена в заголовочный файл, за исключением того случая, когда функция используется только в одном .cpp файле. Если вы помещаете определение встроенной функции в .cpp файл, а вызываете ее из другого .cpp файла, то вы получаете ошибку "unresolved external" ("ненайденный внешний объект") от компоновщика (linker). [9.6] Есть ли другой способ определить встроенную функцию - член класса? Да, определите функцию-член класса в теле самого класса: class Fred { public: void f(int i, char c) { // ... } }; Хотя такой вид определения проще для создателя класса, но он вызывает определенные трудности для пользователя, поскольку здесь смешивается, что делает класс и как он это делает. Из-за этого неудобства предпочтительно определять функции-члены класса вне тела класса, используя слово inline [ 9.5 ]. Причина такого предпочтения проста: как правило, множество людей используют созданный вами класс, но только один человек пишет его (вы); предпочтительно делать вещи, облегчающие жизнь многим [9.7] Обязательно ли встроенные функции приведут к увеличению производительности? Нет. Слишком большое количество встроенных функций может привести к увеличению размера кода, что в свою очередь может оказать негативное влияние на скорость в системах со страничной организацией памяти. РАЗДЕЛ [10]: Конструкторы [10.1] Что такое конструкторы? Конструкторы делают объекты из ничего. Конструкторы похожи на инициализирующие функции. Они превращают свалку случайных бит в работающий объект. В минимальном случае, они инициализируют используемые переменные класса. Также они могут выделять ресурсы (память, файлы, флажки, сокеты и т. п.). "ctor" - часто используемое сокращение для слова конструктор. [10.2] Есть ли разница между объявлениями List x; и List x();? Огромная! Предположим, что List - это имя класса. Тогда функция f() объявляет локальный объект типа List с именем x : void f() { List x; // Локальный объект с именем x (класса List) // ... } Но функция g() объявляет функцию x() , которая возвращает объект типа List : void g() { List x(); // Функция с именем x (возвращающая List) // ... } [10.3] Как из одного конструктора вызвать другой конструктор для инициализации этого объекта? (Имеются в виду несколько перегруженных конструкторов для одного объекта - примечание переводчика.) Никак. Проблема вот в чем: если вы вызовете другой конструктор, компьютер создаст и проинициализирует временный объект, а не объект, из которого вызван конструктор. Вы можете совместить два конструктора, используя значения параметров по умолчанию, или вы можете разместить общий для двух конструкторов код в закрытой ( private ) функции - члене init() . [10.4] Всегда ли конструктор по умолчанию для Fred выглядит как Fred::Fred()? Нет. Конструктор по умолчанию - это конструктор, который можно вызывать без аргументов. Таким образом, конструктор без аргументов безусловно является конструктором по умолчанию: class Fred { public: Fred(); // Конструктор по умолчанию: может вызываться без аргументов // ... }; Однако возможно (и даже вероятно), что конструктор по умолчанию может принимать аргументы, при условии что для всех них заданы значения по умолчанию: class Fred { public: Fred(int i=3, int j=5); // Конструктор по умолчанию: может вызываться без аргументов // ... }; [10.5] Какой конструктор будет вызван, если я создаю массив объектов типа Fred? Конструктор по умолчанию [ 10.4 ] для класса Fred (за исключением случая, описанного ниже) Не существует способа заставить компилятор вызвать другой конструктор (за исключением способа, описанного ниже). Если у вашего класса Fred нет конструктора по умолчанию [ 10.4 ], то при попытке создания массива объектов типа Fred вы получите ошибку при компиляции. class Fred { public: Fred(int i, int j); // ... предположим, что для класса Fred нет конструктора по умолчанию [10.4]... }; int main() { Fred a[10]; // ОШИБКА: У Fred нет конструктора по умолчанию Fred* p = new Fred[10]; // ОШИБКА: У Fred нет конструктора по умолчанию } Однако если вы создаете, пользуясь STL [ 32.1 ], vector<Fred> вместо простого массива (что вам скорее всего и следует делать, поскольку массивы опасны [ 21.5 ]), вам не нужно иметь конструктор по умолчанию в классе Fred , поскольку вы можете задать объект типа Fred для инициализации элементов вектора: #include <vector> using namespace std; int main() { vector<Fred> a(10, Fred(5,7)); // Десять объектов типа Fred // будут инициализированы Fred(5,7). // ... } Хотя вам следует пользоваться векторами, а не массивами, иногда бывают ситуации, когда необходим именно массив. Специально для таких случаев существует способ записи явной инициализации массивов. Вот как это выглядит: class Fred { public: Fred(int i, int j); // ... предположим, что для класса Fred // нет конструктора по умолчанию [10.4]... }; int main() { Fred a[10] = { Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7) }; // Десять объектов массива Fred // будут инициализированы Fred(5,7). // ... } Конечно, вам не обязательно использовать Fred(5,7) для каждого элемента. Вы можете использовать любые числа или даже параметры и другие переменные. Суть в том, что такая запись (a) возможна, но (б) не так хороша, как запись для вектора. Помните: массивы опасны [ 21.5 ]. Если у вы не вынуждены использовать массивы - используйте вектора. [10.6] Должны ли мои конструкторы использовать "списки инициализации" или "присваивания значений"? Конструкторы должны инициализировать все члены в списках инициализации. Например, пусть конструктор инициализирует член x_ , используя список инициализации: Fred::Fred() : x_(какое-то-выражение) { } . С точки зрения производительности важно заметить, что какое-то-выражение не приводит к созданию отдельного объекта для копирования его в x_ : если типы совпадают, то какое-то-выражение будет создано прямо в x_ . Напротив, следующий конструктор использует присваивание: Fred::Fred() { x_ = какое-то-выражение; } . В этом случае какое-то-выражение приводит к созданию отдельного временного объекта, который потом передается в качестве параметра оператору присваивания объекта x_ , а потом уничтожается при достижении точки с запятой. Это неэффективно. Есть и еще один источник неэффективности: во втором случае (с присваиванием) конструктор по умолчанию для объекта (неявно вызванный до { тела конструктора) мог, например, выделить по умолчанию некоторое количество памяти или открыть файл. Вся эта работа окажется проделанной впустую, если какое-то-выражение и/или оператор присваивания привели к закрытию этого файла и/или освобождению памяти (например, если конструктор по умолчанию выделил недостаточно памяти или открыл не тот файл). Выводы: при прочих равных условиях ваш код будет более быстрым, если вы используете списки инициализации, а не операторы присваивания. [10.7] Можно ли пользоваться указателем this в конструкторе? Некоторые люди не рекомендуют использовать указатель this в конструкторе, потому что объект, на который указывает this еще не полностью создан. Тем не менее, при известной осторожности, вы можете использовать this в конструкторе (в {теле} и даже в списке инициализации [ 10.6 ). Как только вы попали в {тело} конструктора, легко себе вообразить, что можно использовать указатель this , поскольку все базовые классы и все члены уже полностью созданы. Однако даже здесь нужно быть осторожным. Например, если вы вызываете виртуальную функцию (или какую-нибудь функцию, которая в свою очередь вызывает виртуальную функцию) для этого объекта, мы можете получить не совсем то, что хотели [ 23.1 ]. На самом деле вы можете пользоваться указателем this даже в списке инициализации конструктора [ 10.6], при условии что вы достаточно осторожны, чтобы по ошибке не затронуть каких-либо объектов-членов или базовых классов, которые еще не были созданы. Это требует хорошего знания деталей порядка инициализации в конструкторе, так что не говорите, что вас не предупреждали. Самое безопасное - сохранить где-нибудь значение указателя this и воспользоваться им потом. [Не понял, что они имеют в виду. - YM] [10.8] Что такое "именованный конструктор" ("Named Constructor Idiom")? Это техника обеспечивает более безопасный и интуитивно понятный для пользователей процесс создания для вашего класса. Проблема заключается в том, что конструкторы всегда носят то же имя, что и их класс. Таким образом, единственное различие между конструкторами одного класса - это их список параметров. И существует множество случаев, когда разница между конструкторами становится весьма незначительной, что ведет к ошибкам. Для использования именованных конструкторов вы объявляете все конструкторы класса в закрытом ( private: ) или защищенном ( protected: ) разделе, и пишете несколько открытых ( public: ) статических методов, которые возвращают объект. Эти статические методы и называются "именованными конструкторами". В общем случае существует по одному такому конструктору на каждый из различных способов создания класса. Например, допустим, у нас есть класс Point , который представляет точку на плоскости X - Y. Существуют два распространенных способа задания двумерных координат: прямоугольные координаты (X + Y) и полярные координаты (радиус и угол). (Не беспокойтесь, если вы не разбираетесь в таких вещах, суть примера не в этом. Суть в том, что существует несколько способов создания объекта типа Point .) К сожалению, типы параметров для этих двух координатных систем одни и те же: два числа с плавающей точкой. Это привело бы к неоднозначности, если бы мы сделали перегруженные конструкторы: class Point { public: Point(float x, float y); // Прямоугольные координаты Point(float r, float a); // Полярные координаты (радиус и угол) // ОШИБКА: Неоднозначная перегруженная функция: Point::Point(float,float) }; int main() { Point p = Point(5.7, 1.2); // Неоднозначность: Какая координатная система? } Одним из путей решения этой проблемы и являются именованные конструкторы: #include <math.h> // Для sin() и cos() class Point { public: static Point rectangular(float x, float y); // Прямоугольные координаты static Point polar(float radius, float angle); // Полярные координаты // Эти статические члены называются "именованными конструкторами" // ... private: Point(float x, float y); // Прямоугольные координаты float x_, y_; }; inline Point::Point(float x, float y) : x_(x), y_(y) { } inline Point Point::rectangular(float x, float y) { return Point(x, y); } inline Point Point::polar(float radius, float angle) { return Point(radius*cos(angle), radius*sin(angle)); } Теперь у пользователей класса Point появился способ ясного и недвусмысленного создания точек в обеих системах координат: int main() { Point p1 = Point::rectangular(5.7, 1.2); // Ясно, что прямоугольные координаты Point p2 = Point::polar(5.7, 1.2); // Ясно, что полярные координаты } Обязательно помещайте ваши конструкторы в защищенный ( protected: ) раздел, если вы планируете создавать производные классы от Fred . [Видимо, ошибка. Хотели сказать - Point . - YM] Именованные конструкторы также можно использовать том в случае, если вы хотите, чтобы ваши объекты всегда создавались динамически (посредством new [ 16.19 ]). [10.9] Почему я не могу проинициализировать статический член класса в списке инициализации конструктора? Потому что вы должны отдельно определять статические данные классов. Fred.h: class Fred { public: Fred(); // ... private: int i_; static int j_; }; Fred.cpp (или Fred.C, или еще как-нибудь): Fred::Fred() : i_(10), // Верно: вы можете (и вам следует) // инициализировать переменные - члены класса таким образом j_(42) // Ошибка: вы не можете инициализировать // статические данные класса таким образом { // ... } // Вы должны определять статические данные класса вот так: int Fred::j_ = 42; [10.10] Почему классы со статическими данными получают ошибки при компоновке? Потому что статические данные класса должны быть определены только в одной единице трансляции [ 10.9 ]. Если вы не делаете этого, вы вероятно получите при компоновке ошибку "undefined external" ("внешний объект не определен"). Например: // Fred.h class Fred { public: // ... private: static int j_; // Объявляет статическую переменную Fred::j_ // ... }; Компоновщик пожалуется ("Fred::j_ is not defined" / "Fred::j_ не определено"), если вы не напишите определение (в отличие от просто объявления) Fred::j_ в одном (и только в одном) из исходных файлов: // Fred.cpp #include "Fred.h" int Fred::j_ = некоторое_выражение_приводимое_к_int; // По-другому, если вы желаете получить неявную инициализацию нулем для int: // int Fred::j_; Обычное место для определения статических данных класса Fred - это файл Fred.cpp (или Fred.C, или другое используемое вами расширение). |