eManual.ru - электронная документация
Секция 2 из 3 - Предыдущая - Следующая
Все секции
- 1
- 2
- 3
[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, или другое используемое
вами расширение).
Секция 2 из 3 - Предыдущая - Следующая
|