12.11 – Файлы исходного кода и заголовочные файлы классов
Определение функций-членов вне определения класса
Все классы, которые мы написали на данный момент, были достаточно простыми, чтобы мы могли реализовать функции-члены непосредственно внутри определения самого класса. Например, вот наш вездесущий класс Date
:
class Date
{
private:
int m_year;
int m_month;
int m_day;
public:
Date(int year, int month, int day)
{
setDate(year, month, day);
}
void setDate(int year, int month, int day)
{
m_year = year;
m_month = month;
m_day = day;
}
int getYear() { return m_year; }
int getMonth() { return m_month; }
int getDay() { return m_day; }
};
Однако по мере того, как классы становятся длиннее и сложнее, наличие всех определений функций-членов внутри класса может затруднить управление классом и работу с ним. Использование уже написанного класса требует понимания только его открытого интерфейса (общедоступных функций-членов), а не понимания того, как класс работает под капотом. Детали реализации функций-членов просто мешают.
К счастью, C++ предоставляет способ отделить часть «объявления» класса от его «реализации». Это делается путем определения функций-членов класса вне определения класса. Для этого просто определите функции-члены класса, как если бы они были обычными функциями, но добавив к функциям префикс из имени класса с помощью оператора разрешения области видимости (::
) (так же, как для пространства имен).
Вот наш класс Date
с конструктором Date
и функцией setDate()
, определенными вне определения класса. Обратите внимание, что прототипы этих функций всё еще присутствуют внутри определения класса, но фактическая реализация вынесена за его пределы:
class Date
{
private:
int m_year;
int m_month;
int m_day;
public:
Date(int year, int month, int day);
void setDate(int year, int month, int day);
int getYear() { return m_year; }
int getMonth() { return m_month; }
int getDay() { return m_day; }
};
// Конструктор Date
Date::Date(int year, int month, int day)
{
SetDate(year, month, day);
}
// Функция-член Date
void Date::SetDate(int year, int month, int day)
{
m_month = month;
m_day = day;
m_year = year;
}
Это довольно просто. Поскольку функции доступа часто представляют собой только одну строку, они обычно остаются в определении класса, даже если их можно переместить за его пределы.
Вот еще один пример, который включает внешний конструктор со списком инициализации членов.
Было:
class Calc
{
private:
int m_value = 0;
public:
Calc(int value=0): m_value(value) {}
Calc& add(int value) { m_value += value; return *this; }
Calc& sub(int value) { m_value -= value; return *this; }
Calc& mult(int value) { m_value *= value; return *this; }
int getValue() { return m_value ; }
};
Стало:
class Calc
{
private:
int m_value = 0;
public:
Calc(int value=0);
Calc& add(int value);
Calc& sub(int value);
Calc& mult(int value);
int getValue() { return m_value; }
};
Calc::Calc(int value): m_value(value)
{
}
Calc& Calc::add(int value)
{
m_value += value;
return *this;
}
Calc& Calc::sub(int value)
{
m_value -= value;
return *this;
}
Calc& Calc::mult(int value)
{
m_value *= value;
return *this;
}
Помещение определения класса в заголовочный файл
Из урока, посвященного заголовочным файлам, вы узнали, что вы можете помещать объявления функций в заголовочные файлы, чтобы использовать эти функции в нескольких файлах или даже в нескольких проектах. Классы ничем не отличаются. Определения классов можно помещать в заголовочные файлы, чтобы упростить их повторное использование в нескольких файлах или нескольких проектах. Традиционно определение класса помещается в заголовочный файл с тем же именем, что и класс, а функции-члены, определенные вне класса, помещаются в файл .cpp с тем же именем, что и класс.
Вот снова наш класс Date
, разбитый на файлы .cpp и .h:
Date.h:
#ifndef DATE_H
#define DATE_H
class Date
{
private:
int m_year;
int m_month;
int m_day;
public:
Date(int year, int month, int day);
void setDate(int year, int month, int day);
int getYear() { return m_year; }
int getMonth() { return m_month; }
int getDay() { return m_day; }
};
#endif
Date.cpp:
#include "Date.h"
// Конструктор Date
Date::Date(int year, int month, int day)
{
SetDate(year, month, day);
}
// Функция-член Date
void Date::setDate(int year, int month, int day)
{
m_month = month;
m_day = day;
m_year = year;
}
Теперь любой другой заголовочный файл или файл исходного кода, который хочет использовать класс Date
, может просто выполнить #include "Date.h"
. Обратите внимание, что Date.cpp также необходимо скомпилировать в проекте, использующем Date.h, чтобы компоновщик знал, как реализован Date
.
Не нарушает ли определение класса в заголовочном файле правило одного определения?
Не должно. Если ваш заголовочный файл имеет надлежащие средства защиты заголовка, то не должно быть возможности включить определение класса более одного раза в один и тот же файл.
Типы (которые включают в себя классы) не подпадают под действие правила одного определения, которое гласит, что у вас может быть только одно определение в программе. Следовательно, нет проблем с включением через #include
определений классов в несколько файлов исходного кода (если бы они были, от классов было бы мало пользы).
Не нарушает ли определение функций-членов в заголовке правило одного определения?
По-разному. Функции-члены, определенные внутри определения класса, считаются неявно встраиваемыми (inline). Встраиваемые (inline) функции освобождаются от правила одного определения относительно одного определения в программе. Это означает, что нет проблем с определением тривиальных функций-членов (таких как функции доступа) внутри определения самого класса.
Функции-члены, определенные вне определения класса, обрабатываются как обычные функции и подчиняются правилу одного определения относительно одного определения в программе. Следовательно, эти функции должны быть определены в файле исходного кода, а не внутри заголовка. Единственным исключением являются шаблоны функций, о которых мы поговорим в следующей главе.
Итак, что я должен определять в файле заголовка, а что в файле .cpp,и что внутри определения класса, а что вне его?
У вас может возникнуть соблазн поместить все определения ваших функций-членов в заголовочный файл внутри класса. Хотя это будет компилироваться, у этого подхода есть несколько недостатков. Во-первых, как упоминалось выше, это загромождает определение вашего класса. Во-вторых, если вы измените что-либо в коде в заголовке, вам придется перекомпилировать каждый файл, который включает этот заголовок. Это может иметь волновой эффект, когда одно незначительное изменение вызывает необходимость перекомпиляции всей программы (что может быть медленным). Если вы измените код в файле .cpp, необходимо будет перекомпилировать только этот файл .cpp!
Поэтому мы рекомендуем следующее:
- классы, используемые только в одном файле, и которые, как правило, нельзя использовать повторно, определяйте непосредственно в том файле .cpp, где они используются;
- классы, используемые в нескольких файлах или предназначенные для повторного использования, определяйте в файле .h с тем же именем, что и класс;
- тривиальные функции-члены (тривиальные конструкторы или деструкторы, функции доступа и т.д.) могут быть определены внутри класса;
- нетривиальные функции-члены должны быть определены в файле .cpp с тем же именем, что и класс.
В будущих уроках большинство наших классов будет определено в файле .cpp, а все функции будут реализованы непосредственно в определении класса. Это сделано для удобства и для краткости примеров. В реальных проектах классы гораздо чаще помещаются в их собственные исходные и заголовочные файлы, и вы должны привыкнуть к этому.
Параметры по умолчанию
Параметры по умолчанию для функций-членов должны быть объявлены в определении класса (в заголовочном файле), где они могут быть видны всем, кто включает заголовок.
Библиотеки
Разделение определения и реализации класса очень распространено для библиотек, которые вы можете использовать для расширения возможностей своей программы. Во всех своих программах вы включаете заголовки, которые принадлежат стандартной библиотеке, например, iostream
, string
, vector
, array
и другие. Обратите внимание, что вам не нужно добавлять в свои проекты iostream.cpp, string.cpp, vector.cpp или array.cpp. Вашей программе нужны объявления из файлов заголовков, чтобы компилятор мог проверить, что вы пишете синтаксически правильные программы. Однако реализации для классов, принадлежащих стандартной библиотеке C++, содержатся в предварительно скомпилированном файле, который подключается на этапе компоновки. Вы никогда не видите кода.
Помимо какого-либо программного обеспечения с открытым исходным кодом (где предоставляются файлы .h и .cpp), большинство сторонних библиотек предоставляют только файлы заголовков и предварительно скомпилированный файл библиотеки. Для этого есть несколько причин:
- линковка предварительно скомпилированной библиотеки выполняется быстрее, чем перекомпиляция ее каждый раз, когда она вам понадобится;
- одна копия предварительно скомпилированной библиотеки может использоваться несколькими приложениями, тогда как скомпилированный код компилируется в каждый исполняемый файл, который его использует (увеличение размеров файлов);
- соображения интеллектуальной собственности (вы не хотите, чтобы украли ваш код).
Разделение ваших собственных файлов на объявление (заголовок) и реализацию (файл исходного кода) – это не только хорошая организация проекта, но оно также упрощает создание ваших собственных пользовательских библиотек. Создание собственных библиотек выходит за рамки данной серии руководств, но разделение объявления и реализации является предварительным условием для этого.