Краткий вопросник по 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, или другое используемое вами расширение).

Читать часть 3


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=21645